#0_채팅방 설계_Socket 통신

마자나다·2023년 8월 21일

채팅방 프로젝트

목록 보기
2/6

채팅방 설계

N:N 채팅방을 자바 코드로 구현해볼 예정이다. 처음부터 끝까지 프로토콜을 모두 만들어 볼것이다. 구현을 위해서 처음 배운 기능이나 설계 방식에 대해서 모두 정리해보자 그에따른 첫번째로 소켓 통신이다.

Socket 통신

  • 소켓 통신이란?
  1. TCP/IP기반 네트워크 통신에서 데이터 송수신의 마지막 접점을 말한다.
  2. 서버 - 클라이언트간 데이터를 주고받는 양방향 연결 지향성 통신을 말한다. 나는 여기에서 클라이언트가 데이터를 전송하면 서버는 그 데이터를 해석한 다음 모든 클라이언트에게 뿌리는 형식으로 구현할 예정이다.
  3. 보통 지속적으로 연결을 유지하면서 실시간으로 데이터를 주고받아야하는 경우에 사용이 된다.

자바의 Blocking 소켓과 non-Blocking소켓

자바의 소켓은 블로킹 소켓과 논블로킹 소켓, 2가지가 있다. 블로킹과 논블로킹 개념에 대해서 짧게 얘기하고 다음 포스팅에 소켓과 논블리킹 소켓
그리고 동기와 비동기 4개의 개념을 동시에 정리하겠다. 내가 크게 헷갈렸던 개념이기 때문에 한꺼번에 다루는 게 좋을듯하다.

  1. Blocking 소켓
    자바의 소켓은 기본적으로 블로킹모드로 동작한다. 블로킹 소켓은 데이터를 읽거나 쓸 때, 소켓 작업이 완료 될때까지 해당 작업에서 대기(Block)하는 방식이다.
    _ 예를 들어 A함수과 B함수이 있다. 작업을 진행하기 위해선 제어권이 필요하다 A가 먼저 제어권을 들고 작업을 하다가,
    _ B를 호출을 하게 되면 B에게 제어권을 넘겨주게 된다.
    _ 제어권이 없는 A는 작업을 멈추게 되고 B가 완료하고 제어권을 다시 돌려주기전까지 대기하게 된다.
    _ B가 완료되면 다시 제어권을 받아와서 A함수를 실행하게 된다.

  1. Non-Blocking 소켓
    반면에 논 블로킹 소켓은 소켓 작업이 완료 되지 않아도 해당 작업에서 바로 반환되는 방식이다.
    논 블로킹은 A함수가 B함수를 호출해도 제어권은 그대로 자신이 가지고 있는다.
    _ A함수가 B함수를 호출하면, B함수는 실행되지만, 제어권은 A함수가 그대로 가지고 있는다.
    _ A함수가 계속 제어권을 가지고 있기 때문에, B함수를 호출한 이후에도 자신의 코드를 계속 실행한다.

간단하게 제어권이 어떻게 옮겨가냐에 따라 블로킹과 논블로킹을 나누었다. 개념이 그렇게 어렵진않다. 동기와 비동기가 같이 들어가면 헷갈릴만한 요소가 많음으로 다음 포스팅에 묶어서 전부 정리하겠다.

참고한 사이트

https://velog.io/@nittre/블로킹-Vs.-논블로킹-동기-Vs.-비동기
  1. 블로킹 논블로킹의 특징.
    블로킹 논블로킹은 java.net패키지, java.nio패키지에서 모두 제공하고 있다.
  • 블로킹 소켓
    블로킹 방식이기 때문에 사용하고 설계하기가 논블로킹에 비해 쉽다.
    클라이언트가 많아질 때마다 늘어나는 스레드는 서버에 부담을 준다. 예전엔 규모가 커지면 논블로킹을 사용한다고 했지만 요즘은 계속된 하드웨어의 발전때문에 블로킹소켓을 거의 사용한다
  • 논블로킹 소켓.
    블로킹보다 쓰기가 까다로운 편이다 (논블로킹이 쓸일이 있으면 Netty를 많이 사용한다.)
    요즘은 거의 사용되지 않는다.

Socket 패키지

  1. Socket 클래스
    클라이언트 측에서 사용된다. 클라이언트에서 서버에 연결할 때 사용된다.
    객체를 생성하여 특정 서버의 IP주소와 포트 번호로 연결을 할 수 있다.
    송수신을 위한 입출력 스트림을 제공하고, 스트림을 통해 서버와 데이터를 주고 받을 수 있다.
    public class Client {
    private static final int SERVER_PORT = 8888; //원하는 포트의 번호(서버 포트와 동일 해야함)
    public static void main(String[] args) {
        Client client = new Client();
        client.start();
    }

    public void start() {

        Socket socket;
        try {
            //socket 연결 후 클라이언트 인풋,아웃풋 생성
            socket = new Socket("localhost", SERVER_PORT); //소켓 객체 생성
    
            ClientOutputThread clientoutputThread = new ClientOutputThread(socket,connectname);
            ClientInputThread clientinputThread = new ClientInputThread(socket,clientoutputThread);
	// 인풋과 아웃풋을 따로 스레드를 2개 만들어서 동작함.
            clientoutputThread.start();
            clientinputThread.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

위 main클래스에서 소켓을 특정포트와 연결해 생성 한 다음, 인풋과 아웃풋 스레드 2개를 만들어 통신을 준비한다.

    public class ClientOutputThread extends Thread {
    Socket socket;
    OutputStream out = null;
    Scanner scanner = new Scanner(System.in);
    String clientname;
    public ClientOutputThread(Socket socket,String clientname) {
        this.socket = socket;
        this.clientname = clientname;
    }


    @Override
    public void run() {
        try {
            out = socket.getOutputStream(); //데이터 전송을 위한 스트림
            startChat();
          } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

위는 아웃풋 스레드로 받아온 소켓을 out = socket.getOutputStream(); 을 만들어, 데이터 전달을 수행한다.

    public class ClientInputThread extends Thread {
    static final int MAXBUFFERSIZE = 2048;
    Socket socket;
    InputStream in = null;
    ClientOutputThread clientOutputThread;

    public ClientInputThread(Socket socket, ClientOutputThread clientOutputThread) {
        this.socket = socket;
        this.clientOutputThread = clientOutputThread;
    }

    @Override
    public void run() {
        try {
            in = socket.getInputStream(); // 데이터를 받기위한 스트림.

            while (true) {
                byte[] serverbytedata = new byte[MAXBUFFERSIZE];
                int serverbytelength = in.read(serverbytedata);
                PacketType serverpackettype = byteToPackettype(serverbytedata);//서버 헤더부분 타입추출
                int serverpacketlength = byteToBodyLength(serverbytedata); //서버 헤더부분 길이추출
                boolean disconnectcheck;
                if (serverbytelength >= 0) {
                    HeaderPacket packet = makeServerPacket(serverbytedata, serverpackettype);
                    disconnectcheck = packetCastingAndPrint(packet, serverpackettype);
                    if(!disconnectcheck){
                        break;
                    }
                }
            }

위 클래스는 input 스레드의 일부분으로 서버보내는 데이터를 해석하는데 사용된다. 아웃풋과 마찬가지로 소켓을 받아와 in = socket.getInputStream();을 만들어 데이터를 받아온다.

  1. ServerSocket 클래스
    서버측에서 사용된다. 서버에서 클라이언트의 연결을 수락하는데 사용되는 클래스 이다.
    특정 포트에서 클라이언트의 연결 요청을 기다린다
    ServerSocket객체가 연결 요청을 수락하면 연결된 클라이언트와 통신하기 위한 Socket을 생성한다

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

    public void start() {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(SERVER_PORT); // 포트번호와 서버 소켓 생성
            System.out.println("[Server Start]");
            while (true) {
                System.out.println("[Client Waiting]");
                Socket socket = serverSocket.accept(); //클라이언트 연결 수락
                //연결이 들어올때마다 새로운 소켓 생성
                //클라이언트가 접속하면 새로운 스레드 생성.
                ServerThread ServerThread = new ServerThread(socket);
                ServerThread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

위 클래스는 클라이언트에서 요청하는 연결을 수락을 도와주는 서버이다. 코드의 주석에서 보면 알수 있듯이
Socket socket = serverSocket.accept();
이 코드에서 클라이언트의 연결요청이 들어오면 새로운 소켓을 생성하고, 밑에선 새로운 스레드를 생성하고 있다.

  • 위에서 사용된 다양한 메서드
  1. .accept() : 서버소켓에서 클라이언트의 연결을 수락한다. 연결 요청이 들어오지 않으면 대기상태로 있는다
  2. .getOutputStream() : 문자열 기반의 입력을 처리하기 위한 클래스로, 주로 텍스트 파일이나 네트워크의 입력을 읽어오는데 사용된다.
  3. .getInputStream() : Socket객체에서 입력 스트림을 가져온다. 소켓으로부터 도착하는 데이터를 읽는데 사용된다.

소켓관련 다양한 스트림 종류

  1. InputStream(입력 스트림) : 데이터를 읽어오는 스트림이다. 다양한 형태의 데이터를 읽어들이는 메서드를 제공한다.
  2. OutputStream(출력 스트림) : 데이터를 쓰거나 보내는 스트림이다. 다양한 형태의 데이터를 출력하는 메서드를 제공한다.
  3. BufferedInputStream : 버퍼링된 입력 스트림으로, 다양한 기능으로 입출력 작업에 더 효율적으로 처리 해준다.
  4. BufferedOutputStream : 버퍼링된 출력 스트림으로서, 출력 작업을 더 효율적으로 처리할 수 있도록 도와줍니다.
  5. ObjectInputStream: 객체를 읽어오는 스트림입니다. Java Serialization을 사용하여 객체를 직렬화하고 역직렬화할 수 있습니다.
  6. ObjectOutputStream: 객체를 쓰거나 보내는 스트림입니다. Java Serialization을 사용하여 객체를 직렬화하고 역직렬화할 수 있습니다.
  7. PrintWriter: 텍스트 데이터를 출력하는 스트림입니다. 문자열 형태로 데이터를 출력할 때 주로 사용합니다.
  8. Scanner: 텍스트 데이터를 읽어오는 스트림입니다. 다양한 데이터 타입을 텍스트로부터 읽어들일 때 사용합니다

자바에서 위처럼 다양한 데이터 스트림이 있지만 내가 만들 프로토콜은 자바로 이루어진 서버와 클라이언트 통신의 목적이 아닌 다양한 언어들이 사용할 수 있도록 설계할 예정이다. 그렇기 때문에 모든 데이터들은 byte단위로 이루어져서 패킷을 주고받으며 통신을 할 수 있도록 설계할 예정이다.

profile
우왕좌왕 개발

0개의 댓글