본문 바로가기
JAVA

TCP 프로그래밍

by 융디's 2024. 4. 27.
728x90
TCP 프로그래밍

TCP 프로그래밍

@2024.04.22

자바 TCP 프로그래밍

💡
java.net package를 사용하여 TCP 기반의 네트워크 프로그래밍을 구현
  • 주로 사용 클래스

    ServerSocket class

    💡
    서버 측에서 클라이언트의 연결 요청을 기다리는데 사용
    • ServerSocket serverSocket = new ServerSocket(PORT) : 서버 소켓
    • Socket clientSocket = serverSocket.accept() : 클라이언트의 연결 수락

    Socket class

    💡
    서버와 클라이언트 간의 연결을 나타내며, 데이터 송수신에 사용
    • Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT) : 서버 연결
    • socket.getInputStream() : 소켓으로부터 데이터를 읽어오는데 사용되는 입력 스트림 반환
    • socket.getOutputStream() : 소켓에 데이터를 쓰는데 사용되는 출력 스트림 반환

    InetAddress class

    💡
    자바에서 IP 주소를 표현하고 관리하기 위해 사용되며, IP 주소를 추상화하여 제공
    • 객체를 직접 생성할 수 없고, 해당 클래스에 속해있는 여러 정적 메서드를 통해 객체를 얻는다.
      • getByName(String host)
        • 주어진 호스트 이름 또는 IP 주소에 대한 InetAddress 객체 반환
      • getAllByName(String host)
        • 주호진 호트 이름에 대한 모든 IP 주소를 InetAddress 배열로 반환
      • getLocalHost()
        • 시스템에서 구성된 호스트 이름과 IP 주소를 포함하는 InetAddress 객체 반환
      • getHostName()
        • InetAddress 객체가 나타내는 호스트 이름 반환
      • getHostAddress()
        • 호스트의 IP 주소를 문자열 형태로 반환
      • getAddress()
        • 호스트의 IP 주소를 바이트 배열로 반환
    • 예시
      • UnknownHostException 예외 : 호스트 이름을 IP 주소로 해석할 수 없을 때 발생
    import java.net.InetAddress;
    
    public class InetAddressExample {
      public static void main(String[] args) {
          
          try {
    					// 특정 호스트의 IP 주소 조회
              InetAddress address = InetAddress.getByName("www.google.com");
              System.out.println("Google's IP Address: " + address.getHostAddress());
              
    					// 로컬 호스트의 IP 주소와 이름 조회
              InetAddress localHost = InetAddress.getLocalHost();
              System.out.println("Local Host Name: " + localHost.getHostName());
              System.out.println("Local IP Address: " + localHost.getHostAddress());
          } catch (Exception e) {
              e.printStackTrace();
          }     
      }
    }
    출력 결과
    Google's IP Address: [Google의 IP 주소]
    Local Host Name: [로컬 호스트의 이름]
    Local IP Address: [로컬 호스트의 IP 주소]
  • TCP 프로그래밍 절차
    1. 서버 생성
      • ServerSocket을 생성하고, 특정 포트에서 클라이언트 연결을 기다린다.
    1. 클라이언트 연결 요청
      • 클라이언트는 Socket을 사용하여 서버의 특정 포트로 연결을 요청한다.
    1. 연결 수락 및 데이터 교환
      • 서버는 클라이언트 요청을 수락하고, Socket을 통해 데이터를 송수신한다.
    1. 연결 종료
      • 데이터 전송이 끝나면 클라이언트와 서버 모두 연결을 종료한다.

Echo 프로그래밍 - TCP 편

💡
클라이언트가 서버에 메시지를 보내면, 서버가 동일한 메시지를 클라이언트에게 되돌려주는 간단한 네트워크 애플리케이션
  • 실행 방법
    1. EchoServer 클래스를 실행하여 서버를 시작
    1. EchoClient 클래스를 실행하여 클라이언트를 서버에 연결
    1. 클라이언트에서 메시지를 입력하고 엔터를 누르면, 서버가 동일한 메시지를 되돌려준다.

단일 클라이언트를 받아들이는 EchoServer

[ Echo Server ]

  1. ServerSocket 객체 생성
    • 특정 포트에서 클라이언트의 연결 요청을 기다리기 위한 ServerSocket 객체 생성
    // 포트 번호 9999를 사용하여 새로운 서버 소켓 생성
    // 클라이언트의 연결 요청을 수신하고, 해당 포트 번호를 통해 서버에 연결할 수 있게 된다.
    ServerSocket server = new ServerSocket(9999);
  1. 클라이언트 연결 수락
    • 클라이언트 연결 요청이 들어오면, accept()를 통해 연결을 수락하고 Socket 객체 생성
     // 서버 소켓에서 클라이언트의 연결을 수락하고, 클라이언트와 통신하기 위한 소켓을 생성
     Socket sc = server.accept();
  1. 데이터 읽고 쓰기
    • 클라이언트로부터 데이터를 읽기 위해 InputStream 사용 (입구)
    • 데이터를 클라이언트에 보내기 위해 OutputStream 사용 (출구)
    /* 
    클라이언트로부터 데이터를 읽어오기 위해 입력 스트림을 설정하고, 
    그 데이터를 텍스트 형식으로 읽어오기 위해 BufferedReader을 생성 
    */
    InputStream in = sc.getInputStream();
    BufferedReader br = new BufferedReader(new InputStreamReader(in));
    
    /* 
    클라이언트에 데이터를 보내기 위해 출력 스트림을 설정하고,
    PrintWriter를 사용하여 서버에서 클라이언트로 데이터를 텍스트 형식으로 전송
    */
    OutputStream out = sc.getOutputStream();
    // PrintWriter pw = new PrintWriter(new OutputStreamWriter(out),true);
    // 두 번째 매개변수에 true를 주면 자동으로 flush 처리
    // PrintWriter의 목적지를 클라이언트 소켓으로 설정
    PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));
    
    // 클라이언트가 보낸 메시지를 읽고, 해당 메시디를 그대로 클라이언트에게 다시 보낸다. 
    String line = null;
    while((line=br.readLine()) != null){
        System.out.println("클라이언트에서 받은 메시지 : " + line);
        pw.println("server ::: " + line); // 서버에서 클라이언트로 메시지 전송
        pw.flush(); // 버퍼를 비워서 즉서 전송
    }
  1. 연결 종료
    • 클라이언트와의 통신이 끝나면, Socket을 닫아 연결을 종료
      • 클라이언스 소켓 : 서버에서 해당 클라이언트로 더 이상 통신이 불가능
      • 서버 소켓 : 서버가 더 이상 클라이언트의 연결을 받지 않을 것이라면, 서버 소켓도 단는다. (더 이상 클라이언트의 연결 요청을 수락하지 않음)
    server.close();
    sc.close();
    br.close();;
    pw.close();
  • 전체 코드
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class EchoServer {
      public static void main(String[] args) {
          final int PORT = 9999; // 포트 번호
          
          try {
              // 서버 소켓 생성
              ServerSocket serverSocket = new ServerSocket(PORT);
              System.out.println("Echo Server is running on port " + PORT);
              
              // 클라이언트의 연결을 대기하고 수락
              Socket clientSocket = serverSocket.accept();
              System.out.println("Client connected: " + clientSocket.getInetAddress().getHostAddress());
              
              // 클라이언트와 통신을 위한 입출력 스트림 생성
              BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
              PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
              
              // 클라이언트로부터 메시지 수신 및 다시 전송
              String message;
              while ((message = in.readLine()) != null) {
                  System.out.println("Received message from client: " + message);
                  out.println("Echo from server: " + message);
              }
              
              // 연결 종료
              System.out.println("Client disconnected.");
              clientSocket.close();
              serverSocket.close();
              
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
    }

[ Echo Client ]

  1. Socket 객체 생성
    • 서버의 IP 주소와 포트 번호를 사용하여 Socket 객체를 생성하여 서버에 연결
      // 새로운 클라이언트 소켓을 생성한다.
      // 이 소켓은 127.0.0.1 주소와 포트 번호 9999로 지정된 서버에 연결된다. 
      Socket sock = new Socket("127.0.0.1", 9999);
  1. 데이터 전송 및 응답 수신
    • 서버로부터의 응답을 받기 위해 InputStream을 사용 (입구)
    • 서버에 데이터를 보내기 위해 OutputStream을 사용 (출구)
      // 클라이언트 소켓을 통해 서버로부터 데이터를 읽어오기 위한 BufferedRreader 설정
      BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
      
      // 클라이언트 소켓을 통해 서버로 데이터를 보내기 위한 PrintWriter 설정
      PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
      
      //클라이언트가 키보드를 통해 입력하기위한 통로
      BufferedReader keybord = new BufferedReader(new InputStreamReader(System.in));
      
      String line = null;
      while ((line = keybord.readLine()) != null) {
          if (line.equalsIgnoreCase("quit")) {
              break;
          }
          //서버에게 보냄.
          pw.println(line);
          pw.flush();
      
          //서버에서 받음
          String echo = br.readLine();
          System.out.println("서버로부터 받은 응답 메시지 : " + echo);
      }
  1. 연결 종료
    • 데이터 송수신이 끝나면 Socket을 닫아 연결을 종료
      pw.close();
      br.close();
      keybord.close();
      sock.close();
  • 전체 코드
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.Socket;
    
    public class EchoClient {
        public static void main(String[] args) {
            final String SERVER_IP = "127.0.0.1"; // 서버 IP 주소
            final int SERVER_PORT = 9999; // 서버 포트 번호
    
            try {
                // 서버에 연결
                Socket socket = new Socket(SERVER_IP, SERVER_PORT);
                System.out.println("Connected to server.");
    
                // 클라이언트에서 서버로 메시지를 보낼 PrintWriter
                PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
                // 서버에서 클라이언트로 메시지를 받을 BufferedReader
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                // 키보드 입력을 받을 BufferedReader
                BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
    
                String line = null;
                while ((line = keybord.readLine()) != null) {
                    if (line.equalsIgnoreCase("quit")) {
                        break;
                    }
                    //서버에게 보냄.
                    pw.println(line);
                    pw.flush();
    
                    //서버에서 받음
                    String echo = br.readLine();
                    System.out.println("서버로부터 받은 응답 메시지 : " + echo);
                }
    
                // 연결 종료
                socket.close();
                System.out.println("Disconnected from server.");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

다중 클라이언트를 받아드리는 EchoServer

Thread를 이용하면 된다!
  • 구현 방법
    1. 클라이언트 연결을 수락하기 위한 ServerScoket 생성
    1. 클라이언트가 연결될 때마다 새로운 스레드를 생성하여 해당 클라이언트의 요청을 처리
    1. 각 클라이언트 스레드는 해당 클라이언트와 통신하고, 클라이언트로부터 받은 데이터를 다른 모든 클라이언트에게 다시 전송
    import java.io.BufferedReader;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class MultiEchoServer {
        public static void main(String[] args) {
            final int PORT = 9999;
            
            try {
                ServerSocket serverSocket = new ServerSocket(PORT);
                System.out.println("Multi Echo Server is running on port " + PORT);
                
                while (true) {
                    // 클라이언트 연결을 수락 -> 클라이언트 수 만큼 반복
                    Socket clientSocket = serverSocket.accept();
                    System.out.println("Client connected: " + clientSocket.getInetAddress().getHostAddress());
                    
                    // 클라이언트와 통신하는 스레드 시작 
                    // 클라이언트마다 각자 실행 할 수 있도록 만들어야한다
                    ClientThread client = new ClientThread(clientSocket);
                    clientThread.start();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    class ClientHandler extends Thread {
        private Socket clientSocket;
        
        public ClientHandler(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }
        
        @Override
        public void run() {
            try {
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
                
                String message;
                while ((message = in.readLine()) != null) {
                    System.out.println("Received message from client: " + message);
                    
                    // 클라이언트로부터 받은 메시지를 다시 보냄
                    out.println("Echo from server: " + message);
                }
                
                // 연결 종료
                System.out.println("Client disconnected: " + clientSocket.getInetAddress().getHostAddress());
                clientSocket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

채팅 프로그램 만들기

  • 다수의 클라이언트가 서버에 동시에 연결되어 서로 메시지를 주고받을 수 있다.
  • 고려 사항
    • 스레드 동기화
      • 다수의 클라이언트가 동시에 메시지를 보낼 때, 데이터의 일관성을 유지하기 위해 필요
    • 오류 처리
      • 네트워크 오류나 사용자 연결 종료와 같은 예외 상황 처리
    • 효율적인 리소스 관리
      • 클라이언트 연결 종료 시 리소스를 정리하고, 서버의 부하를 관리해야 한다.

[ Echo Server ]

  1. ServerSocket 설정
    • ServerSocket을 이용하여 특정 포트에 클라이언트의 연결을 기다린다.
  1. 클라이언트 연결 처리
    • 클라이언트 연결 요청이 들어오면 accept() 메서드를 통해 이를 수락한다.
    • 각 클라이언트마다 별도의 스레드를 생성하여 독립적으로 처리한다.
  1. 메시지 중계
    • 서버는 클라이언트로부터 받은 메시지를 다른 클라이언트에게 전달(브로드캐스트)
        // 전체 사용자에게 메시지 보내기 메소드 (브로드캐스트)
        public void broadcast(String msg){
    //        for(PrintWriter out : chatClients.values()){
    //            out.println(msg);
    //        }
            synchronized (chatClients){
                Iterator<PrintWriter> it= chatClients.values().iterator();
                while(it.hasNext()){
                    PrintWriter out = it.next();
                    try{
                        out.println(msg);
                    }catch (Exception e){
                        it.remove(); // 브로드 캐스트 할 수 없는 사용자를 삭제
                        e.printStackTrace();
                    }
                }
            }
        }
  • 전체 코드1 (수업)
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map;
    
    public class ChatServer {
        public static void main(String[] args) {
            // 1. 서버 소켓 생성
            try(ServerSocket server = new ServerSocket(9999);){
                // 여러명의 클라이언트의 정보를 기억할 공간
                Map<String, PrintWriter> chatClients  = new HashMap<>();
                System.out.println("서버 준비 완료");
                while(true){
                    // 2. accept()를 통해서 소켓 얻어옴 (여러명의 클라이언트 접속)
                    Socket socket = server.accept();
                    //Thread 이용
                    new ChatThread(socket,chatClients).start();
                }
            }catch (Exception e){
                e.getStackTrace();
            }
    
    
        }
    }
    
    class ChatThread extends Thread{
      private Socket socket; // 소켓
      private String id; // 사용자를 구별할 ID
      private Map<String,PrintWriter> chatClients; // 여러명의 클라이언트
        // 생성자를 통해서 클라이언트 소켓을 얻어옴
        // 각각 클라이언트와 통신 할 수 있는 통로를 얻어온다.
        BufferedReader in;
        PrintWriter out;
        public ChatThread(Socket socket, Map<String,PrintWriter> chatClients){
            this.socket = socket;
            this.chatClients = chatClients;
            // 클라이언트가 생성될때 클라이언트로부터 아이디를 얻어오게 하고싶다.
            try{
                in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                out = new PrintWriter(socket.getOutputStream(),true);
                id = in.readLine();
                // 모든 사용자에게 id님이 입장했다는 정보를 알려준다.
                broadcast(id + "님이 입장하셨습니다.");
                System.out.println("새로운 사용자의 아이디는 " + id + "입니다.");
                // 동시에 여러 클라이언트가 put 할 수도 있다 -> 동기화 필요(syncronized)
                synchronized (chatClients){
                    chatClients.put(this.id,out);
                }
            }catch(Exception e){
                System.out.println(e);
            }
    
        } // 생성자 끝
    
      
        // 연결된 클라이언트가 메시지를 전송하면, 그 메시지를 받아서 다른 사용자에게 보내줌
        @Override
        public void run() {
            String msg = null;
            try{
                while((msg = in.readLine())!=null){
                    broadcast(id + " : " + msg);
                }
            }catch (IOException e){
                System.out.println(e);
            }finally {
                synchronized (chatClients){
                    chatClients.remove(id);
                }
                broadcast(id + "님이 채팅에서 나갔습니다.");
    
                if(in !=null){
                    try{
                        in.close();
                    }catch(IOException e){
                        throw new RuntimeException(e);
                    }
                }
               if(socket != null){
                   try{
                       socket.close();
                   }catch(IOException e){
                       throw new RuntimeException(e);
                   }
               }
    
            }
        }
    
        // 전체 사용자에게 메시지 보내기 메소드 (브로드캐스트)
        public void broadcast(String msg){
    //        for(PrintWriter out : chatClients.values()){
    //            out.println(msg);
    //        }
            synchronized (chatClients){
                Iterator<PrintWriter> it= chatClients.values().iterator();
                while(it.hasNext()){
                    PrintWriter out = it.next();
                    try{
                        out.println(msg);
                    }catch (Exception e){
                        it.remove(); // 브로드 캐스트 할 수 없는 사용자를 삭제
                        e.printStackTrace();
                    }
                }
            }
        }
    }
  • 전체 코드2
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.HashSet;
    import java.util.Set;
    
    public class PdfServer {
        private static final int PORT = 12345;
        private static Set<PrintWriter> allCients = new HashSet<>();
    
        public static void main(String[] args) throws Exception {
            System.out.println("채팅 서버가 시작되었습니다.");
            ServerSocket listener = new ServerSocket(PORT);
    
            try{
                while(true){
                    new Handler(listener.accept()).start();
                }
            }finally {
                listener.close();
            }
        }
    
        private static class Handler extends Thread{
            private Socket socket;
            private PrintWriter out; // 서버를 기준으로 출력이면, 클라이언트에겐 입력
            private BufferedReader in; // 서버를 기준으로 입력이면, 클라이언트에겐 출력
    
            public Handler(Socket socket){
                this.socket = socket;
            }
    
            @Override
            public void run() {
                try{
                    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    out = new PrintWriter(socket.getOutputStream(), true);
    
                    synchronized (allCients){
                        allCients.add(out);
                    }
                    String input;
                    while((input=in.readLine())!=null){
                        synchronized (allCients){
                            for(PrintWriter writer : allCients){
                                writer.println(input);
                            }
                        }
                    }
                }catch(IOException e){
                    System.out.println(e.getMessage());
                }finally {
                    if(out != null){
                        synchronized (allCients){
                            allCients.remove(out);
                        }
                    }
                    try{
                        socket.close();
                    }catch (IOException e){
                        System.out.println(e.getMessage());
                    }
                }
            }
        }
    }
    
    

[ Echo Client ]

  1. Socket을 이용한 서버 연결
    • 클라이언트는 서버의 IP 주소와 포트 번호를 사용하여 서버에 연결한다,
  1. 메시지 송수신
    • 콘솔에서 입력받은 메시지를 서버에 보내고, 서버로부터 오는 메시지를 콘솔에 출력한다.
  • 전체 코드1 (수업)
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.Socket;
    
    public class ChatClient {
        public static void main(String[] args) {
            //아이디가 처음에 입력되게 하기 위해서 args[0] 에서 받아오는 것으로 구현해봅시다.
            if (args.length != 1) {
                System.out.println("사용법 : java ChatClent id");
                System.exit(1);
            }
    
            try (Socket socket = new Socket("127.0.0.1", 9999);
                 PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
                 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                 BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
            ) {
                //접속되면 id를 서버에 보낸다. (서버와의 약속!!)
                out.println(args[0]);
    
                //네트워크에서 입력된 내용을 담당하는 부분을 Thread로..
                new InputThread(socket, in).start();
    
                //키보드로부터 입력받은 내용을 서버에 보내는코드
                String msg = null;
                while ((msg = keyboard.readLine()) != null) {
                    out.println(msg);
                    if ("/quit".equalsIgnoreCase(msg))
                        break;
                }
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }
    
    class InputThread2 extends Thread {
        private Socket socket;
        private BufferedReader in;
    
        InputThread2(Socket socket, BufferedReader in) {
            this.socket = socket;
            this.in = in;
        }
    
        @Override
        public void run() {
            try {
                String msg = null;
                while ((msg = in.readLine()) != null) {
                    System.out.println(msg);
                }
            } catch (Exception e) {
                System.out.println(e);
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
    
  • 전체 코드2
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.Socket;
    import java.util.Scanner;
    
    public class PdfClient {
        private static final String SERVER_ADDRESS = "localhost";
        private static final int SERVER_PORT = 12345;
    
        public static void main(String[] args) throws Exception{
            try(Socket socket = new Socket(SERVER_ADDRESS,SERVER_PORT)){
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                PrintWriter out = new PrintWriter(socket.getOutputStream(),true);
                Scanner sc = new Scanner(System.in);
    
    
                Thread listenerThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            String serverMessage;
                            while((serverMessage = in.readLine())!=null){
                                System.out.println("서버 "  + serverMessage);
                            }
                        }catch (IOException e){
                            System.out.println(e.getMessage());
                        }
                    }
                });
                listenerThread.start();
    
                while(true){
                    System.out.println("메시지 입력 : ");
                    String message = sc.nextLine();
                    if(message.equalsIgnoreCase("exit")){
                        break;
                    }
                    out.println(message);
                }
            }
        }
    }
    

728x90

'JAVA' 카테고리의 다른 글

익명 객체  (0) 2024.04.27
UDP 프로그래밍  (0) 2024.04.27
네트워크의 기본  (0) 2024.04.27
멀티 스레드  (2) 2024.04.27
java.io 패키지  (0) 2024.04.27