JAVA

네트워크 - 프로그램1

경딩 2025. 1. 29. 22:57

네트워크 프로그램 1 - 예제

이제 본격적으로 자바 네트워크 프로그램을 작성해 보자.

여기서는 TCP/IP로 개발할 예정이다. (UDP는 직접 사용할 일이 많지 않으므로 다루지 않겠다.

 

이번에 만들 프로그램은 아주 간단한 네트워크 클라이언트, 서버 프로그램이다.

클라이언트가 "Hello"라는 문자를 서버에 전달하면 서버는 클라이언트의 요청에 " World!"라는 단어를 더해서 "Hello World!"라는 단어를 클라이언트에 응답한다

  • 클라이언트 서버: "Hello"
  • 클라이언트 서버: "Hello World!"
package network.tcp.v1;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

import static util.MyLogger.log;

public class ClientV1_1 {


    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("클라이언트 시작");
        Socket socket = new Socket("localhost", PORT); // TCP/IP 가 외부 네트워크 연결해줌 (소켓 연결됨)
        DataInputStream input = new DataInputStream(socket.getInputStream()); // 소켓에 있는 스트림을 꺼내면 외부와 통신할 수 있음
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());
        socket.getOutputStream(); // 외부에 데이터를 보낼 수 있음
        socket.getInputStream(); // 외부에 데이터를 읽어올 수 있음
        log("소켓 연결: " + socket);

        // 서버에게 문자 보내기
        String toSend = "Hello";
        output.writeUTF(toSend);
        log("client ->  server: " + toSend);

        // 서버로부터 문자 받기
        String received = input.readUTF();
        log("client <-  server: " + received);

        // 자원 정리
        log("연결 종료 : " + socket);
        input.close();
        output.close();
        socket.close();

    }
}

 

 

package network.tcp.v1;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import static util.MyLogger.log;

public class ServerV1_1 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("서버 시작");
        ServerSocket serverSocket = new ServerSocket(PORT);
        log("서버 소켓 시작 - 리스닝 포트 : " + PORT);

        Socket socket = serverSocket.accept(); // 12345 번 포트에 누군가 접속하면 소켓을 만듦 - 이 소켓을 통해 클라이언트와 서버가 통신할 수 있게 됨
        log("소켓 연결: " + socket);

        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());

        // 클라이언트로부터 문자 받기
        String received = input.readUTF();
        log("client -> server : " + received);

        // 클라이언트에게 문자 보내기
        String toSend = received + " World";
        output.writeUTF(toSend);
        log("client <- server : " + toSend);

        // 자원 정리
        log(" 연결 종료  : " + socket);
        output.close();
        input.close();
        socket.close();
        serverSocket.close();
    }
}

 

서버를 먼저 실행하고, 그다음에 클라이언트를 실행해야 한다.

 

실행 결과 - 클라이언트

 

실행 결과 - 서버

 

localhost, 127.0.0.1

  • localhost는 현재 사용 중인 컴퓨터 자체를 가리키는 특별한 호스트 이름이다.
  • google.com, naver.com과 같은 호스트 이름이지만, 자기 자신의 컴퓨터를 뜻하는 이름이다.
  • localhost는 127.0.0.1이라는 IP로 매핑된다.
  • 127.0.0.1은 IP 주소 체계에서 루프백 주소(loopback address)로 지정된 특별한 IP 주소이다. 이 주소는 컴퓨 터가 스스로를 가리킬 때 사용되며, "localhost"와 동일하게 취급된다
  • 127.0.0.1은 컴퓨터가 네트워크 인터페이스를 통해 외부로 나가지 않고, 자신에게 직접 네트워크 패킷을 보낼 수 있도록 한다

 

네트워크 프로그램 1 - 분석

TCP / IP 통신에서는 통신할 대상 서버를 찾을 때 호스트 이름이 아니라 ip 주소가 필요하다.

네트워크 프로그램을 분석하기 전에 먼저 호스트 이름으로 IP를 어떻게 찾는지 확인해 보자.

package network.tcp.v1;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressMain1 {
    public static void main(String[] args) throws UnknownHostException {
        InetAddress localhost = InetAddress.getByName("localhost");
        System.out.println(localhost);

        InetAddress google = InetAddress.getByName("google.com");
        System.out.println(google);
    }
}

자바의 ` InetAddress ` 클래스를 사용하면 호스트 이름으로 대상 IP를 찾을 수 있다.

찾는 과정은 다음과 같다.

  • 자바는 ` InetAddress.getByName("호스트명") 메서드를 사용해서 해당하는 IP 주소를 조회한다.
  • 이 과정에서 시스템의 호스트 파일을 먼저 확인한다.
    • /etc/hosts  (리눅스, mac)
    • \Windows\System32\drivers\etc\hosts (윈도, Windows)
  • 호스트 파일에 정의되어 있지 않다면 면, DNS 서버에 요청해서 IP 주소를 얻는다.

 

호스트 파일 - 예시 

 127.0.0.1
 localhost
 255.255.255.255 broadcasthost
 ::1             localhost

 

클라이언트 코드 분석

클라이언트와 서버의 연결은 Socket을 사용한다.

 

Socket socket = new Socket("localhost", PORT);

 

  • localhost를 통해 자신의 컴퓨터에 있는 12345 포트에 TCP 접속을 시도한다.
    • localhost는 IP 가 아니므로 해당하는 IP 를 먼저 찾는다. 내부에서 InetAddress 를 사용한다.
    • localhost 는 127.0.0.1이라는 IP에 매핑되어 있다.
    • 127.0.0.1:12345에 TCP 접속을 시도한다.
  • 연결이 성공적으로 완료되면 Socket 객체를 반환한다.
  • Socket 은 서버와 연결되어 있는 연결점이라고 생각하면 된다.
  • Socket 객체를 통해서 서버와 통신할 수 있다.

클라이언트와 서버 간의 데이터 통신은 Socket 이 제공하는 스트림을 사용한다.

 

        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());

Socket 은 서버와 데이터를 주고받기 위한 스트림을 제공한다.

InputStream: 서버에서 전달한 데이터를 클라이언트가 받을 때 사용한다.

OutputStream : 클라이언트에서 서버에 데이터를 전달할 때 사용한다.

InputStream, OutputStream을 그대로 사용하면 모든 데이터를 byte 로 변환해서 전달해야하기 때문에 번거롭다. 여기서는 DataInputStream, DataOutputStream 이라는 보조 스트림을 사용해서, 자바 타입의 메시지를 편리하게 주고 받을 수 있다.

 

        // 서버에게 문자 보내기
        String toSend = "Hello";
        output.writeUTF(toSend);
        log("client -> server: " + toSend);

 

OutputStream 을 통해 서버에 "Hello" 메시지를 전송한다.

        // 서버로부터 문자 받기
        String received = input.readUTF();
        log("client <- server: " + received);

 

InputStream을 통해 서버가 전달한 메시지를 받을 수 있다.

클라이언트가 "Hello"를 전송하면 서버는 " World!"라는 문자를 붙여서 반환하므로 "Hello World!"라는 문자를 반환받는다.

 

사용한 자원은 반드시 정리해야 한다.

        // 자원 정리
        log("연결 종료: " + socket);
        input.close();
        output.close();
        socket.close();

 

 

서버 코드 분석

서버 소켓 

서버는 특정 포트를 열어두어야 한다. 그래야 클라이언트가 해당 포트를 지정해서 접속할 수 있다.

    ServerSocket serverSocket = new ServerSocket(PORT);

 

서버는 서버 소켓 (ServerSocket)이라는 특별한 소켓을 사용한다.

지정한 포트를 사용해서 서버 소켓을 생성하면, 클라이언트는 해당 포트로 서버에 연결할 수 있다.

 

클라이언트와 서버의 연결과정을 그림으로 자세히 알아보자.

 

 

서버가 12345 포트로 서버 소켓을 열어둔다. 클라이언트는 이제 12345 포트로 서버에 접속할 수 있다.

클라이언트가 12345 포트에 연결을 시도한다.

이때 OS 계층에서 TCP 3 way handshack 가 발생하고, TCP 연결이 완료된다.

TCP 연결이 완료되면 서버는 OS backlog queue라는 곳에 클라이언트와 서버의 TCP 연결 정보를 보관한다.

이 연결 정보를 보면 클라이언트의 IP, PORT , 서버의 IP, PORT 정보가 모두 들어 있다.

 

클라이언트와 랜덤 포트

TCP 연결 시 클라이언트 서버 모두 IP, PORT 정보가 필요하다. 예제에서 사용된 IP 포트는 다음과 같다

클라이언트: localhost(127.0.0.1),  50000 (포트 랜덤 생성)

서버: localhost(127.0.0.1) , 12345

 

클라이언트 자신의 포트를 지정한 적이 없다.

서버의 경우 포트가 명확하게 지정되어 있어야 한다. 그래야 클라이언트에서 서버에 어떤 포트에 접속할지 알 수 있다.

반면에 서버에 접속하는 클라이언트의 경우에는 자신의 포트가 명확하게 지정되어 있지 않다도 된다.

클라이언트는 보통 포트를 생략하는데, 생략할 경우 클라이언트 PC에 남아 있는 포트 중 하나가 랜덤으로 할당된다.

참고로 클라이언트의 포트도 명식적을 할당할 수 있지만 잘 사용하지 않는다.

 

 

accept()

        Socket socket = serverSocket.accept();

서버 소켓은 단지 클라이언트와 서버의 TCP 연결만 지원하는 특별할 소켓이다.

실제 클라이언트와 서버가 정보를 주고받으려면 Socket 객체가 필요하다. (서버 소켓이 아니다! 소켓이다!!)

serverSocket.accept() 메서드를 호출하면 TCP 연결 정보를 기반으로, Socket 객체를 만들어서 반환한다.

 

accpet() 호출 과정을 그림으로 자세히 알아보자

 

 

accept()를 호출하면 backlog queue에서 TCP 연결 정보를 조회한다.

만약 TCP 연결정보가 없다면, 연결정보가 생성될 때까지 대기한다. (블로킹)

해당 정보를 기반으로 Socket 객체를 생성한다.

사용한 TCP 연결 정보는 backlog queue 에서 제거된다.

 

Socket 생성 후 그림

 

 

 

클라이언트와 서버의 Socket 은 TCP로 연결되어 있고, 스트림을 통해 메시지를 주고받을 수 있다.

 

     DataInputStream input = new DataInputStream(socket.getInputStream());
     DataOutputStream output = new DataOutputStream(socket.getOutputStream());

 

  • Socket 은 클라이언트와 서버가 데이터를 주고받기 위한 스트림을 제공한다.
  • InputStream : 서버 입장에서 보면 클라이언트가 전달한 데이터를 서버가 받을 때 사용한다.
  • OutputStream : 서버에서 클라이언트에 데이터를 전달할 때 사용한다.

클라이언트의 Output 은 서버의 input이고 반대로 서버의 Output 서버의 Input이다.

자신을 기준으로 생각하면 된다.

       // 클라이언트로부터 문자 받기
        String received = input.readUTF();
  • 클라이언트가 전달한 "Hello" 메시지를 전달받는다.
        // 클라이언트에게 문자 보내기
        String toSend = received + " World!";
        output.writeUTF(toSend);

클라이언트의 메시지에 " World!" 메시지를 붙여서 반환한다.

OutputStream을 통해 서버에서 클라이언트로 메시지를 전송한다.

        // 자원 정리
        log("연결 종료: " + socket);
        input.close();
        output.close();
        socket.close();
        serverSocket.close();

필요한 자원을 사용하고 나면 꼭! 정리해야 한다.

 

문제 이 프로그램은 메시지를 하나만 주고받으면 클라이언트와 서버가 모두 종료된다. 메시지를 계속 주고받고, 원할 때 종료할 수 있도록 변경해 보자

 

네트워크 프로그램 2 - 예제

이번에는 클라이언트와 서버가 메시지를 계속 주고받다가, "exit"라고 입력하면 클라이언트와 서버를 종료해 보자.

package network.tcp.v2;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

import static util.MyLogger.log;

public class ClientV2_2 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("클라이언트 시작");
        Socket socket = new Socket("localhost", PORT);
        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());
        log("소캣 연결: " + socket);

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("전송 문자 : "  );
            String toSend = scanner.nextLine();

            // 서버에게 문자 보내기
            output.writeUTF(toSend);
            log("client -> server: " + toSend);

            if("exit".equals(toSend)) {
                break;
            }

            // 서버로부터 문자 받기
            String received = input.readUTF();
            log("client <- server: " + received);
        }



        // 자원 정리
        log("연결 종료: " + socket);
        input.close();
        output.close();
        socket.close();
    }
}
  • 클라이언트와 서버가 메시지를 주고받는 부분만 while로 반복하면 된다.
  • exit를 입력하면 클라이언트는 exit 메시지를 서버에 전송하고, 클라이언트는 while 문을 빠져나가면서 연결을 종료한다.
package network.tcp.v2;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import static util.MyLogger.log;

public class ServerV2_1 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("서버 시작");
        ServerSocket serverSocket = new ServerSocket(PORT);
        log("서버 소켓 시작 - 리스닝 포트: " + PORT);

        Socket socket = serverSocket.accept();
        log("소켓 연결: " + socket);
        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());


        while (true) {
            // 클라이언트로부터 문자 받기
            String received = input.readUTF();
            log("client -> server: " + received);

            if("exit".equals(received)) break;

            // 클라이언트에게 문자 보내기
            String toSend = received + " World!";
            output.writeUTF(toSend);
            log("client <- server: " + toSend);
        }


        // 자원 정리
        log("연결 종료: " + socket);
        input.close();
        output.close();
        socket.close();
        serverSocket.close();
    }
}

 

클라이언트와 서버가 메시지를 주고받는 부분만 while 문으로 반복하면 된다.

클라이 언트로 부터 exit 메시지가 전송되면, 서버는 while 문을 빠져나가면서 연결을 종료한다.

 

실행 결과 - 서버 / 클라이언

덕분에 클라이언트와 서버가 필요할 때까지 계속 메시지를 주고받을 수 있다.

 

문제

서버는 하나의 클라이언트가 아니라, 여러 클라이언트의 연결을 처리할 수 있어야 한다.

여러 클라이언트가 하나의 서버에 접속하도록 해보자.

 

처음 접속한 ClientV2는 문제없이 작동한다. 하지만 중간에 새로 연결한 ClientV2-2는 소켓 연결은 되지만, 메시지를 전송해도 서버로부터 아무런 응답이 오지 않는다.

왜 이런 문제가 발생하는 것일까?

네트워크 프로그램 2 - 분석

서버 소켓과 연결 더 자세히 알아보자. 이번에는 여러 클라이언트가 서버가 접속한다고 가정해 보자.

  • 서버는 12345 서버 소켓을 열어둔다.
  • 5000번 랜덤 포트를 사용하는 클라이언트가 먼저 12345 포트의 서버에 접속을 시도한다.
  • 이때 OS 계층에서 TCP 3 way handshake 가 발생하고, TCP 연결이 완료된다.
  • TCO 연결이 완료되면 서버는 OS backlog queue라는 곳에 클라이언트와 서버의 TCP 연결 정보를 보관한다.

 

여기서 중요한 점이 있는데, 이 시점에서 TCP 3 way handshake 가 완료되었기 때문에 , 클라이언트와 서버의 TCP 연결은 이미 완료되고, 클라이언트의 소켓 객체도 정상 생성된다. 참고로 이 시점에 아직 서버의 소켓 객체 (서버 소켓 아님)는 생성되지 않았다.

 

  • 이번에는 60000번 랜덤 포트를 사용하는 클라이언트가 12345 포트의 서버에 접속을 시도하고 연결을 완료한다.
  • 50000번 클라이언트와 60000번 클라이언트 모두 서버와 연결이 완료되었고, 클라이언트의 소캣도 정상 생성된다.

 

서버가 클라이언트와 데이터를 주고받으려면 소켓을 획득해야 한다.

ServerSocket.accpet() 메서드를 호출하면 backlog 큐의 정보를 기반으로 소켓 객체를 하나 생성한다.

큐이므로 순서대로 데이터를 꺼낸다. 처음 50000번 클라이언트의 접속 정보를 기반으로 서버에 소켓이 하나 생성된다.

50000 번 클라이언트와 서버는 소켓의 스트림을 통해 서로 데이터를 주고받는다.

 

  • 그림에서 6000번 클라이언트도 이미 서버와 TCP 연결은 되어있다.
    • OS 계층에서 TCP 3 way handshake 가 발생하고, TCP 연결이 완료되었다.
  • 6000번  클라이언트도 서버와 TCP 연결이 되었기 때문에 서버로 메시지를 보낼 수 있다.
    • 아직 서버에 Socket 객체가 없더라도 메시지를 보낼 수 있다. TCP 연결은 이미 완료되었다.

그림을 보자. 소켓을 통해 스트림으로 메시지를 주고받는다는 것은 사실은 이러한 과정을 거치는 것이다.

자바 애플리케이션은 소켓 객체의 스트림을 통해 서버와 데이터를 주고받는다. 데이터를 주고 받는 과정은 다음과 같다.

 

클라이언트가 "Hello, world!"라는 메시지를 서버에 전송하는 경우

  • 클라이언트 
    • 애플리케이션 -> OS TCP 송신 버퍼 -> 클라이언트 네트워크 카드
  • 클라이언트가 보낸 메시지가 서버에 도착했을 때, 서버
    • 서버 네트워크 카드 -> OS TCP 수신 버퍼 -> 애플리케이션

여기서 60000번 클라이언트가 보낸 메시지는 서버 애플리케이션에서 아직 읽지 않았기 때문에, 서버 OS의  TCP 수신 버퍼에서 대기하게 된다.

 

여기서 핵심적인 내용은 소켓 객체 없이 서버 소켓만으로도 TCP 연결이 완료된다는 점이다.

(서버 소켓은 연결만 담당한다.)

하지만 연결 이후에 서로 메시지를 주고받으려면 소켓 객체가 필요하다.

 

accept()는 이미 연결된 TCP 연결 정보를 기반으로 서버 측에 소켓 객체를 생성한다. 그리고 이 소켓 객체가 있어야 스트림을 사용해서 메시지를 주고받을 수 있다.

 

 

이렇게 소켓을 연결하면 소켓의 스트림을 통해 OS TCP 수신 버퍼에 있는 메시지를 읽을 수 있고, 또 전송할 수도 있다.

 

accept() 메서드는 backlog 큐에 새로운 연결  정보가 도착할 때까지 블로킹 상태로 대기한다. 새로운 연결 정보가 오지 않으면 계속 대기하는 블로킹 메서드이다.

 

ServerV2의 문제

` ServerV2 ` 에 둘 이상의 클라이언트가 작동하지 않는 이유는 다음과 같다

  • 새로운 클라이언트가 접속하면?
    • 새로운 클라이언트가 접속했을 때 서버의 main 스레드는 accept() 메서드를 절대로 호출할 수 없다!
    • 왜냐하면 while 문으로 기존 클라이언트와 메시지를 주고받는 부분만 반복하기 때문이다.
  • 2개의 블로킹 작업 - 핵심은 별도의 스레드가 필요하다!
    • accept() : 클라이언트와 서버의 연결을 처리하기 위해 대기
    • readXxx() : 클라이언트의 메시지를 받아서 처리하기 위해 대기
    • 각각의 블로킹 작업은  별도의 스레드에서 처리해야 한다. 그렇지 않으면 다른 블로킹 메서드 때문에 계속 대기할 수 있다.

ServerV2 코드 - 간략 

ServerSocket serverSocket = new ServerSocket(PORT);
Socket socket = serverSocket.accept(); // 블로킹


while (true) {
    // 클라이언트로부터 문자 받기
    String received = input.readUTF(); // 블로킹
    output.writeUTF(toSend);
}

 

네트워크 프로그램 3

이번에는 여러 클라이언트가 동시에 접속할 수 있는 서버 프로그램을 작성해 보자.

 

서버의 main 스레드는 서버 소켓을 생성하고, 서버 소켓의 accept()를 반복해서 호출해야 한다.

 

  • 클라이언트가 서버에 접속하면 서버 소켓의  accept() 메서드가 Socket을 반환한다.
  • main 스레드는 이 정보를 기반으로 Runnable을 구현한 Session이라는 변도의 객체를 만들고 , 새로운 스레드에서 이 객체를 생성한다. 여기서는 Thread-0 이 Session을 실행한다.
  • Session 객체와 Thread-0 은 연결된 클라이언트와 메시지를 주고받는다.

 

  • 클라이언트가 서버에 접속하면 서버 소켓의 accept() 메서드가 Socket을 반환한다.
  • main 스레드는 이 정보를 기반으로 Runnable을 구현한 Session이라는 별도의 객체를 만들고, 새로운 스레드에서 이 객체를 실행한다. 여기서 느 Thread-0 이 Session을 실행한다.
  • Session 객체와 Thread-0 은 연결된 클라이언트와 메시지를 주고받는다.

 

 

 

새로운 TCP 연결이 발생하면 main 스레드는 새로운 Session 객체를 별도의 스레드에서 실행한다. 그리고 이 과정을 반복한다.

Session 객체와 Thread-1 은 연결된 클라이언트와 메시지를 주고받는다.

 

역할의 분리

  • main 스레드 
    • main 스레드는 새로운 연결이 있을 때마다 Session 객체와 별도의 스레드를 생성하고, 별도의 스레드가 Session 객체를 실행하도록 한다.
  • Session 담당 스레드
    • Session을 담당하는 스레드는 자신의 소켓이 연결된 클라이언트와 메시지를 반복해서 주고받은 역할을 담당한다.
package network.tcp.v3;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

import static util.MyLogger.log;

public class ClientV3_1 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("클라이언트 시작");
        Socket socket = new Socket("localhost", PORT);
        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());
        log("소캣 연결: " + socket);

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("전송 문자: ");
            String toSend = scanner.nextLine();

            // 서버에게 문자 보내기
            output.writeUTF(toSend);
            log("client -> server: " + toSend);

            if (toSend.equals("exit")) {
                break;
            }

            // 서버로부터 문자 받기
            String received = input.readUTF();
            log("client <- server: " + received);
        }

        // 자원 정리
        log("연결 종료: " + socket);
        input.close();
        output.close();
        socket.close();
    }
}

클라이언트 코드는 기존 코드와 완전히 같다. 클래스 이름만 ClientV2  -> CilentV3_1로 변경했다.

 

package network.tcp.v3;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

import static util.MyLogger.log;

public class SessionV3_1 implements Runnable {

    private final Socket socket;

    public SessionV3_1(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            DataInputStream input = new DataInputStream(socket.getInputStream());
            DataOutputStream output = new DataOutputStream(socket.getOutputStream());

            while (true) {
                // 클라이언트로부터 문자 받기
                String received = input.readUTF();
                log("client -> server: " + received);

                if (received.equals("exit")) {
                    break;
                }

                // 클라이언트에게 문자 보내기
                String toSend = received + " World!";
                output.writeUTF(toSend);
                log("client <- server: " + toSend);
            }

            // 자원 정리
            log("연결 종료: " + socket);
            input.close();
            output.close();
            socket.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
  • Session의 목적은 소켓이 연결된 클라이언트와 메시지를 반복해서 주고받는 것이다.
  • 생성자를 통해 Socket 객체를 입력받는다.
  • Runnable을 구현해서 별도의 스레드에서 실행한다.
package network.tcp.v3;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import static util.MyLogger.log;

public class ServerV3 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("서버 시작");
        ServerSocket serverSocket = new ServerSocket(PORT);
        log("서버 소켓 시작 - 리스닝 포트: " + PORT);

        while (true) {
            Socket socket = serverSocket.accept(); // 블로킹
            log("소켓 연결: " + socket);


            SessionV3_1 session = new SessionV3_1(socket);
            Thread thread = new Thread(session);
            thread.start();
        }

    }
}
  • main 코드는 main 스레드가 작동하는 부분이다.
  • main 스레드는 서버 소켓을 생성하고 , serverSocket.accept()을 호출해서 연결을 대기한다.
  • 새로운 연결이 추가될 때마다 Session 객체를 생성하고 별도의 스레드에서 Session 객체를 실행한다.
  • 이 과정을 반복한다

실행 결과 - ClientV3

 

 

실행 결과 - ClientV3-2

 

실행 결과 - ServerV3 

여러 서버가 접속해도 문제없이 작동하는 것을 확인할 수 있다. 그리고 각각의 연결이 별도의 스레드에서 처리되는 것도 확인 할 수 있다.

 

서버 소켓을 통해 소켓을 연결하는 부분과 각 클라이언트와 메시지를 주고받는 부분이 별도의 스레드로  나뉘어 있다.

블로킹되는 부분은 이렇게 별도의 스레드로 나누어  실행해야 한다.