1. 개요

소켓 프로그래밍 이라는 용어 는 장치가 모두 네트워크를 사용하여 서로 연결된 여러 컴퓨터에서 실행되는 프로그램을 작성하는 것을 말합니다.

소켓 프로그래밍에 사용할 수있는 통신 프로토콜에는 UDP (User Datagram Protocol)와 TCP (Transfer Control Protocol)의 두 가지가 있습니다.

둘 사이의 주요 차이점은 UDP는 연결이 없다는 것입니다. 즉, TCP는 연결 지향적 인 반면 클라이언트와 서버간에 세션이 없다는 것입니다. 즉, 통신이 이루어 지려면 먼저 클라이언트와 서버간에 배타적 연결이 설정되어야합니다.

이 튜토리얼 은 TCP / IP 네트워크를 통한 소켓 프로그래밍에 대한 소개를 제공 하고 Java로 클라이언트 / 서버 애플리케이션을 작성하는 방법을 보여줍니다. UDP는 주류 프로토콜이 아니므로 자주 발생하지 않을 수 있습니다.

2. 프로젝트 설정

Java는 클라이언트와 서버 간의 낮은 수준의 통신 세부 정보를 처리하는 클래스 및 인터페이스 모음을 제공합니다.

이들은 대부분 java.net 패키지에 포함되어 있으므로 다음 가져 오기를 수행해야합니다.

import java.net.*;

또한 통신하는 동안 쓰고 읽을 입력 및 출력 스트림을 제공 하는 java.io 패키지 가 필요합니다 .

import java.io.*;

간단하게하기 위해 동일한 컴퓨터에서 클라이언트 및 서버 프로그램을 실행합니다. 다른 네트워크 컴퓨터에서 실행한다면 IP 주소 만 변경됩니다.이 경우에는 127.0.0.1에서 localhost사용 합니다 .

3. 간단한 예

클라이언트와 서버와 관련된 가장 기본적인 예제로 손을 더럽 히자 . 클라이언트가 서버에 인사하고 서버가 응답하는 양방향 통신 애플리케이션이 될 것입니다.

다음 코드를 사용하여 GreetServer.java 라는 클래스에 서버 애플리케이션을 생성 해 보겠습니다 .

이 기사에서는 모든 서버를 실행하는 방법에주의를 기울일 수 있도록 주요 방법과 전역 변수를 포함합니다 . 기사의 나머지 예제에서는 이러한 종류의 더 반복적 인 코드를 생략합니다.

public class GreetServer {
    private ServerSocket serverSocket;
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        String greeting = in.readLine();
            if ("hello server".equals(greeting)) {
                out.println("hello client");
            }
            else {
                out.println("unrecognised greeting");
            }
    }

    public void stop() {
        in.close();
        out.close();
        clientSocket.close();
        serverSocket.close();
    }
    public static void main(String[] args) {
        GreetServer server=new GreetServer();
        server.start(6666);
    }
}

다음 코드를 사용하여 GreetClient.java 라는 클라이언트도 만들어 보겠습니다 .

public class GreetClient {
    private Socket clientSocket;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String ip, int port) {
        clientSocket = new Socket(ip, port);
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
    }

    public String sendMessage(String msg) {
        out.println(msg);
        String resp = in.readLine();
        return resp;
    }

    public void stopConnection() {
        in.close();
        out.close();
        clientSocket.close();
    }
}

서버를 시작합시다. IDE에서는 Java 애플리케이션으로 실행하기 만하면됩니다.

이제 서버가 실제로 응답으로 인사말을 보내는 지 확인하는 단위 테스트를 사용하여 서버에 인사말을 보내겠습니다.

@Test
public void givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect() {
    GreetClient client = new GreetClient();
    client.startConnection("127.0.0.1", 6666);
    String response = client.sendMessage("hello server");
    assertEquals("hello client", response);
}

여기서 무슨 일이 일어나고 있는지 완전히 이해하지 못하더라도 걱정하지 마십시오.이 예제는이 기사의 뒷부분에서 무엇을 기대해야하는지에 대한 느낌을주기위한 것입니다.

다음 섹션에서는 이 간단한 예제를 사용하여 소켓 통신분석 하고 더 많은 예제를 통해 세부 사항에 대해 자세히 알아볼 것 입니다.

4. 소켓 작동 원리

위의 예를 사용하여이 섹션의 여러 부분을 단계별로 살펴 보겠습니다.

정의에 따라 소켓 은 네트워크의 다른 컴퓨터에서 실행되는 두 프로그램 간의 양방향 통신 링크의 한 끝점입니다. 소켓은 포트 번호에 바인딩되어 있으므로 전송 계층은 데이터가 전송 될 애플리케이션을 식별 할 수 있습니다.

4.1. 서버

일반적으로 서버는 네트워크의 특정 컴퓨터에서 실행되며 특정 포트 번호에 바인딩 된 소켓이 있습니다. 이 경우 클라이언트와 동일한 컴퓨터를 사용하고 포트 6666 에서 서버를 시작했습니다 .

ServerSocket serverSocket = new ServerSocket(6666);

서버는 클라이언트가 연결 요청을 할 때까지 소켓을 듣고 기다립니다. 이것은 다음 단계에서 발생합니다.

Socket clientSocket = serverSocket.accept();

서버 코드가 accept 메소드를 만나면 클라이언트가 연결 요청을 할 때까지 차단됩니다.

모든 것이 잘되면 서버 연결을 수락 합니다. 수락하면 서버는 동일한 로컬 포트 6666에 바인딩 된 새 소켓 clientSocket을 가져오고 원격 엔드 포인트도 클라이언트의 주소 및 포트로 설정합니다.

이 시점에서 새로운 Socket 객체는 서버를 클라이언트와 직접 연결하고 출력 및 입력 스트림에 액세스하여 각각 클라이언트와 메시지를 쓰고받을 수 있습니다.

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

여기서부터 서버는 소켓이 스트림으로 닫힐 때까지 클라이언트와 끝없이 메시지를 교환 할 수 있습니다.

그러나이 예에서 서버는 연결을 닫기 전에 인사말 응답 만 보낼 수 있습니다. 즉, 테스트를 다시 실행하면 연결이 거부됩니다.

통신의 연속성을 허용하려면 while 루프 내에서 입력 스트림을 읽고 클라이언트가 종료 요청을 보낼 때만 종료해야합니다. 다음 섹션에서이 작업을 확인합니다.

모든 새 클라이언트에 대해 서버에는 accept 호출에서 반환 된 새 소켓이 필요합니다 . ServerSocket의이 연결된 고객의 요구하는 경향이있는 동안 연결 요청을 수신하는 데 계속 사용됩니다. 우리는 첫 번째 예에서 아직 이것을 허용하지 않았습니다.

4.2. 클라이언트

클라이언트는 서버가 실행중인 시스템의 호스트 이름 또는 IP와 서버가 수신하는 포트 번호를 알아야합니다.

연결 요청을하기 위해 클라이언트는 서버의 컴퓨터 및 포트에있는 서버와 랑데뷰를 시도합니다.

Socket clientSocket = new Socket("127.0.0.1", 6666);

클라이언트는 또한 서버에 자신을 식별하여이 연결 중에 사용할 시스템에서 할당 한 로컬 포트 ​​번호에 바인딩해야합니다. 우리는 이것을 스스로 다루지 않습니다.

위의 생성자는 서버가 연결 수락 했을 때만 새 소켓을 생성 합니다. 그렇지 않으면 연결 거부 예외가 발생합니다. 성공적으로 생성되면 서버와 통신하기 위해 입력 및 출력 스트림을 가져올 수 있습니다.

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

클라이언트의 입력 스트림은 서버의 입력 스트림이 클라이언트의 출력 스트림에 연결된 것처럼 서버의 출력 스트림에 연결됩니다.

5. 지속적인 커뮤니케이션

현재 서버는 클라이언트가 연결할 때까지 차단 한 다음 다시 차단하여 클라이언트의 메시지를 듣습니다. 단일 메시지 후에는 연속성을 처리하지 않았기 때문에 연결을 닫습니다.

따라서 ping 요청에만 도움이되지만 채팅 서버를 구현하고 싶다면 서버와 클라이언트 간의 지속적인 양방향 통신이 반드시 필요합니다.

들어오는 메시지에 대한 서버의 입력 스트림을 지속적으로 관찰하기 위해 while 루프를 만들어야합니다.

클라이언트로부터받은 모든 메시지를 에코 백하는 것이 유일한 목적인 EchoServer.java 라는 새 서버를 만들어 보겠습니다 .

public class EchoServer {
    public void start(int port) {
        serverSocket = new ServerSocket(port);
        clientSocket = serverSocket.accept();
        out = new PrintWriter(clientSocket.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
        
        String inputLine;
        while ((inputLine = in.readLine()) != null) {
        if (".".equals(inputLine)) {
            out.println("good bye");
            break;
         }
         out.println(inputLine);
    }
}

마침표 문자를 받으면 while 루프가 종료되는 종료 조건을 추가했습니다.

GreetServer에서 했던 것처럼 main 메서드를 사용하여 EchoServer시작 합니다 . 이번에는 혼동을 피하기 위해 4444 와 같은 다른 포트에서 시작합니다 .

EchoClient은 비슷합니다 GreetClient 우리가 코드를 복제 할 수 있습니다. 명확성을 위해 분리하고 있습니다.

다른 테스트 클래스에서는 서버가 소켓을 닫지 않고도 EchoServer 에 대한 다중 요청이 제공된다는 것을 보여주는 테스트를 생성 할 것입니다. 동일한 클라이언트에서 요청을 보내는 한 마찬가지입니다.

여러 클라이언트를 다루는 것은 다른 경우이며, 다음 섹션에서 살펴 보겠습니다.

서버와의 연결을 시작 하는 설정 방법을 만들어 보겠습니다 .

@Before
public void setup() {
    client = new EchoClient();
    client.startConnection("127.0.0.1", 4444);
}

모든 리소스를 해제 하는 tearDown 메서드를 똑같이 생성 할 것입니다 . 이것은 네트워크 리소스를 사용하는 모든 경우에 모범 사례입니다.

@After
public void tearDown() {
    client.stopConnection();
}

그런 다음 몇 가지 요청으로 에코 서버를 테스트 해 보겠습니다.

@Test
public void givenClient_whenServerEchosMessage_thenCorrect() {
    String resp1 = client.sendMessage("hello");
    String resp2 = client.sendMessage("world");
    String resp3 = client.sendMessage("!");
    String resp4 = client.sendMessage(".");
    
    assertEquals("hello", resp1);
    assertEquals("world", resp2);
    assertEquals("!", resp3);
    assertEquals("good bye", resp4);
}

이것은 서버가 연결을 닫기 전에 한 번만 통신하는 초기 예제에 비해 개선 된 것입니다. 이제 세션이 완료되면 서버에 알리기 위해 종료 신호를 보냅니다 .

6. 여러 클라이언트가있는 서버

앞의 예가 첫 번째 예에 비해 개선 된 것처럼 여전히 훌륭한 솔루션은 아닙니다. 서버에는 많은 클라이언트와 많은 요청을 동시에 처리 할 수있는 용량이 있어야합니다.

여러 클라이언트를 처리하는 것은이 섹션에서 다룰 것입니다.

여기서 보게 될 또 다른 기능은 동일한 클라이언트가 연결 거부 예외 또는 서버에서 연결 재설정없이 연결을 끊었다가 다시 연결할 수 있다는 것입니다. 이전에는 이것을 할 수 없었습니다.

이것은 우리 서버가 여러 클라이언트의 여러 요청에 대해 더 강력하고 탄력적이라는 것을 의미합니다.

이 작업을 수행하는 방법은 모든 새 클라이언트에 대해 새 소켓을 만들고 클라이언트가 다른 스레드에서 요청하는 서비스를 제공하는 것입니다. 동시에 제공되는 클라이언트 수는 실행중인 스레드 수와 같습니다.

메인 스레드는 새로운 연결을 수신 할 때 while 루프를 실행합니다.

충분히 얘기하자, EchoMultiServer.java 라는 또 다른 서버를 만들어 보자 . 그 안에는 소켓에서 각 클라이언트의 통신을 관리하는 핸들러 스레드 클래스를 만들 것입니다.

public class EchoMultiServer {
    private ServerSocket serverSocket;

    public void start(int port) {
        serverSocket = new ServerSocket(port);
        while (true)
            new EchoClientHandler(serverSocket.accept()).start();
    }

    public void stop() {
        serverSocket.close();
    }

    private static class EchoClientHandler extends Thread {
        private Socket clientSocket;
        private PrintWriter out;
        private BufferedReader in;

        public EchoClientHandler(Socket socket) {
            this.clientSocket = socket;
        }

        public void run() {
            out = new PrintWriter(clientSocket.getOutputStream(), true);
            in = new BufferedReader(
              new InputStreamReader(clientSocket.getInputStream()));
            
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                if (".".equals(inputLine)) {
                    out.println("bye");
                    break;
                }
                out.println(inputLine);
            }

            in.close();
            out.close();
            clientSocket.close();
    }
}

이제 while 루프 내에서 accept를 호출 합니다. 누구나 매년 시간 동안의 루프가 실행은에 차단 동의를 새로운 클라이언트 연결될 때까지 호출 한 후 핸들러 스레드, EchoClientHandler는 ,이 클라이언트 생성됩니다.

스레드 내부에서 발생하는 일은 이전 에 단일 클라이언트 만 처리 EchoServer 에서 수행 한 작업입니다. 그렇게 EchoMultiServer에 이 작업을 위임 EchoClientHandler 는 더 많은 고객을위한 듣기를 유지할 수 있도록 하는 동안 루프.

우리는 여전히 EchoClient사용 하여 서버를 테스트 할 것입니다. 이번에는 각각 서버에서 여러 메시지를 보내고받는 여러 클라이언트를 만들 것입니다.

포트 5555 에서 기본 방법을 사용하여 서버를 시작하겠습니다 .

명확성을 위해 새 제품군에 테스트를 계속 배치합니다.

@Test
public void givenClient1_whenServerResponds_thenCorrect() {
    EchoClient client1 = new EchoClient();
    client1.startConnection("127.0.0.1", 5555);
    String msg1 = client1.sendMessage("hello");
    String msg2 = client1.sendMessage("world");
    String terminate = client1.sendMessage(".");
    
    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

@Test
public void givenClient2_whenServerResponds_thenCorrect() {
    EchoClient client2 = new EchoClient();
    client2.startConnection("127.0.0.1", 5555);
    String msg1 = client2.sendMessage("hello");
    String msg2 = client2.sendMessage("world");
    String terminate = client2.sendMessage(".");
    
    assertEquals(msg1, "hello");
    assertEquals(msg2, "world");
    assertEquals(terminate, "bye");
}

우리가 원하는만큼 많은 테스트 케이스를 만들 수 있으며, 각각 새 클라이언트를 생성하고 서버는 모든 테스트 케이스를 제공합니다.

7. 결론

이 튜토리얼에서는 TCP / IP를 통한 소켓 프로그래밍대한 소개에 초점을 맞추고 Java로 간단한 클라이언트 / 서버 애플리케이션을 작성했습니다.

기사의 전체 소스 코드는 평소처럼 GitHub 프로젝트 에서 찾을 수 있습니다 .