자바 소켓 가이드

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. 지속적인 커뮤니케이션

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

So it is only helpful in ping requests, but imagine we would like to implement a chat server, continuous back and forth communication between server and client would definitely be required.

We will have to create a while loop to continuously observe the input stream of the server for incoming messages.

Let's create a new server called EchoServer.java whose sole purpose is to echo back whatever messages it receives from clients:

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); } }

Notice that we have added a termination condition where the while loop exits when we receive a period character.

We will start EchoServer using the main method just as we did for the GreetServer. This time, we start it on another port such as 4444 to avoid confusion.

The EchoClient is similar to GreetClient, so we can duplicate the code. We are separating them for clarity.

In a different test class, we shall create a test to show that multiple requests to the EchoServer will be served without the server closing the socket. This is true as long as we are sending requests from the same client.

Dealing with multiple clients is a different case, which we shall see in a subsequent section.

Let's create a setup method to initiate a connection with the server:

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

We will equally create a tearDown method to release all our resources, this is best practice for every case where we use network resources:

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

Let's then test our echo server with a few requests:

@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); }

This is an improvement over the initial example, where we would only communicate once before the server closed our connection; now we send a termination signal to tell the server when we're done with the session.

6. Server With Multiple Clients

Much as the previous example was an improvement over the first one, it is still not that great a solution. A server must have the capacity to service many clients and many requests simultaneously.

Handling multiple clients is what we are going to cover in this section.

Another feature we will see here is that the same client could disconnect and reconnect again, without getting a connection refused exception or a connection reset on the server. Previously we were not able to do this.

This means that our server is going to be more robust and resilient across multiple requests from multiple clients.

How we will do this is to create a new socket for every new client and service that client's requests on a different thread. The number of clients being served simultaneously will equal the number of threads running.

The main thread will be running a while loop as it listens for new connections.

Enough talk, let's create another server called EchoMultiServer.java. Inside it, we will create a handler thread class to manage each client's communications on its socket:

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(); } }

Notice that we now call accept inside a while loop. Every time the while loop is executed, it blocks on the accept call until a new client connects, then the handler thread, EchoClientHandler, is created for this client.

What happens inside the thread is what we previously did in the EchoServer where we handled only a single client. So the EchoMultiServer delegates this work to EchoClientHandler so that it can keep listening for more clients in the while loop.

우리는 여전히 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 프로젝트에서 찾을 수 있습니다.