[Java] 27. 네트워크 통신(Socket)을 하는 방법


Study/Java  2020. 6. 2. 19:45

안녕하세요. 명월입니다.


이 글은 Java에서 네트워크 통신(Socket)을 하는 방법에 대한 글입니다.


프로그램에서 소켓이라고 하면 프로그램과 프로그램 또는 PC와 PC 간의 통신을 이야기합니다.

간단하게 생각하면 통신을 할 때 전송하는 패킷(데이터)이 컴퓨터의 랜 카드를 거쳐 랜 케이블로 나갑니다. 랜 케이블로 나간 데이터는 DNS와 라우터 등을 거쳐 도달하고자 하는 PC의 랜 카드에 들어가고 목표로 하는 프로그램에서 패킷(데이터)를 읽어 서로 간에 데이터를 주고 받습니다.

이 때, 우리는 각 단말 간에 데이터 변환이나 장비 간의 통신 규약에 대해서 모두 개발하지 않습니다. 이러한 통신 규약 등은 모두 OS 측에서 설정(OSI 7계층)되고, 우리는 그 위에 꽂아서 쓴다라는 개념으로 Socket 통신 규약을 이용해 통신합니다.

링크 - [위키백과] OSI 모형


Socket 통신 규약은 일련의 규칙이 정해져 있습니다.

먼저 기다리는 측의 PC를 서버라고 하며 Port를 열고 클라이언트의 접속을 기다립니다. 그리고 접속하는 측을 클라이언트라고 하며 서버의 IP와 Port에 접속하여 통신이 연결이 됩니다.

서버와 클라이언트 간의 통신은 Send, Receive의 형태로 데이터를 주고 받습니다. 그리고 서로 통신이 끝나면 close로 접속을 끊습니다.

출처 - http://jkkang.net/unix/netprg/chap2/net2_1.html


그럼 이러한 소켓의 원리로 Java에서 소켓 통신을 작성하겠습니다.

먼저 Server를 만들고 Window의 Telnet 프로그램을 이용해서 접속을 확인하고 그 사양에 맞추어서 Client을 작성하겠습니다.

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 서버 클래스
public class Server {
  // 버퍼 사이즈 설정
  private final static int BUFFER_SIZE = 1024;
  // 실행 함수
  public static void main(String[] args) {
    // 서버 인스턴스 생성 (프로그램이 종료 될때 자동 close)
    try (ServerSocket server = new ServerSocket()) {
      // 9999 포트에 서버를 대기 시킨다.
      InetSocketAddress ipep = new InetSocketAddress(9999);
      // 서버 인스턴스에 소켓 정보 bind
      server.bind(ipep);
      // 콘솔 출력
      System.out.println("Initialize complate");
      // 클라이언트로 부터 메시지를 대기하는 스래드 풀
      ExecutorService receiver = Executors.newCachedThreadPool();
      // 클라이언트 리스트
      List<Socket> list = new ArrayList<>();
      // 서버는 무한 대기
      while (true) {
        try {
          // 클라이언트로 부터 접속 대기한다.
          Socket client = server.accept();
          // 클라이언트 리스트에 추가한다.
          list.add(client);
          // 접속 정보 콘솔 출력
          System.out.println("Client connected IP address =" + client.getRemoteSocketAddress().toString());
          // 클라이언트 스래드 풀 시작
          receiver.execute(() -> {
            // client가 종료되면 소켓을 close한다.
            // OutputStream과 InputStream를 받는다.
            try (Socket thisClient = client; 
                  OutputStream send = client.getOutputStream(); 
                  InputStream recv = client.getInputStream();) {
              // 메시지 작성
              String msg = "Welcome server!\r\n>";
              // byte 변환
              byte[] b = msg.getBytes();
              // 클라이언트로 전송
              send.write(b);
              // 버퍼
              StringBuffer sb = new StringBuffer();
              // 메시지 대기 루프
              while (true) {
                // 버퍼 생성
                b = new byte[BUFFER_SIZE];
                // 메시지를 받는다.
                recv.read(b, 0, b.length);
                // byte를 String으로 변환
                msg = new String(b);
                // 버퍼에 메시지 추가
                sb.append(msg.replace("\0", ""));
                // 메시지가 개행일 경우 (클라이언트에서 엔터를 친 경우)
                if (sb.length() > 2 && sb.charAt(sb.length() - 2) == '\r' && sb.charAt(sb.length() - 1) == '\n') {
                  // 메시지를 String으로 변환
                  msg = sb.toString();
                  // 버퍼 비우기
                  sb.setLength(0);
                  // 메시지 콘솔 출력
                  System.out.println(msg);
                  // exit 메시지일 경우 메시지 대기 루프를 종료한다.
                  if ("exit\r\n".equals(msg)) {
                    break;
                  }
                  // echo 메시지 작성
                  msg = "echo : " + msg + ">";
                  // byte로 변환
                  b = msg.getBytes();
                  // 클라이언트로 전송
                  send.write(b);
                }
              }
            } catch (Throwable e) {
              // 에러 발생시 콘솔 출력
              e.printStackTrace();
            } finally {
              // 접속이 종료되면 접속 정보를 콘솔 출력
              System.out.println("Client disconnected IP address =" + client.getRemoteSocketAddress().toString());
            }
          });
        } catch (Throwable e) {
          // 에러 발생시 콘솔 출력
          e.printStackTrace();
        }
      }
    } catch (Throwable e) {
      // 에러 발생시 콘솔 출력
      e.printStackTrace();
    }
  }
}

위에서 accept는 while(true)로 무한 루프 안에 넣어서 클라이언트를 대기하고 있습니다.

클라이언트 접속이 되면 스레드 풀에 Socket을 넘겨서 클라이언트로 부터 메시지를 대기합니다.

여기서 Socket으로 Stream을 받아 write, read를 할 수 있는 데 이는 IO와 같은 원리입니다.

링크 - [Java] 26. 파일(IO)를 다루는 방법(파일의 작성, 수정, 사용 날짜 변경과 close를 하는 이유, Closable 사용법)


기동을 하면 Initialize complate 메시지가 콘솔에 출력되고 Listen 상태로 됩니다.

telnet으로 접속하고 메시지를 보내겠습니다.

telnet으로 127.0.0.1 9999로 접속을 해서 hello world를 치고 exit를 치니 접속이 종료된 것을 확인할 수 있습니다.

서버를 보니 접속된 것이 확인이 되네요.


이 서버를 이용해서 클라이언트를 만들어 보겠습니다.

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 클라이언트 클래스
public class Client {
  // 버퍼 사이즈 설정
  private final static int BUFFER_SIZE = 1024;
  // 실행 함수
  public static void main(String[] args) {
    // 클라이언트 인스턴스 생성 (프로그램이 종료 될때 자동 close)
    try (Socket client = new Socket()) {
      // 로컬:9999 포트에 서버에 접속한다.
      InetSocketAddress ipep = new InetSocketAddress("127.0.0.1", 9999);
      // 접속
      client.connect(ipep);
      // OutputStream과 InputStream를 받는다.
      try (OutputStream send = client.getOutputStream(); 
           InputStream recv = client.getInputStream();) {
        // 콘솔 출력
        System.out.println("Client connected IP address =" + client.getRemoteSocketAddress().toString());
        // 메시지 받기 스레드 풀
        ExecutorService receiver = Executors.newSingleThreadExecutor();
        receiver.execute(() -> {
          try {
            // 메시지 무한 대기
            while (true) {
              // 버퍼 생성
              byte[] b = new byte[BUFFER_SIZE];
              // 메시지를 받는다.
              recv.read(b, 0, b.length);
              // 콘솔 출력
              System.out.println(new String(b));
            }
          } catch (Throwable e) {
            // 에러 콘솔 출력
            e.printStackTrace();
          }
        });
        // 콘솔로 메시지 받기
        try (Scanner sc = new Scanner(System.in)) {
          // 콘솔 메시지 무한 대기
          while (true) {
            // 메시지를 받는다.
            String msg = sc.next() + "\r\n";
            // byte 변환
            byte[] b = msg.getBytes();
            // 서버로 메시지 전송
            send.write(b);
            // exit일 경우 접속 종료
            if ("exit\r\n".equals(msg)) {
              break;
            }
          }
        }
      }
    } catch (Throwable e) {
      // 에러 발생시 콘솔 출력
      e.printStackTrace();
    }
  }
}

이클립스에서는 동시에 두개의 main을 실행할 수 없으니 서버는 jar로 export한 후 커맨더에서 실행하겠습니다.

이제 이클립스를 기동해서 서버에 접속하겠습니다.

정상 접속이 되서 메시지를 보내면 에코 메시지도 도착합니다. exit를 하면 서버와 접속도 정상적으로 종료가 되네요. 에러 Exception이 발생은 합니다만, 정상 종료가 되어서 발생한 것입니다.

서버에서도 정상 접속되고 메시지를 받고 종료된 것을 확인할 수 있습니다.


서버와 클라이언트의 소스를 보면 크게 차이가 나지 않습니다. 서버는 ServerSocket 인스턴스를 생성해서 접속을 하면 Socket 인스턴스를 리턴하네요.

클라이언트에서는 Socket 인스턴스를 생성해서 접속합니다. 결국에는 서로 메시지 주고 받는 것은 이 Socket 클래스에서 하게 됩니다.


여기까지 Java에서 네트워크 통신(Socket)을 하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.