728x90
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 주소를 바이트 배열로 반환
- getByName(String host)
- 예시
- 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 프로그래밍 절차
- 서버 생성
- ServerSocket을 생성하고, 특정 포트에서 클라이언트 연결을 기다린다.
- 클라이언트 연결 요청
- 클라이언트는 Socket을 사용하여 서버의 특정 포트로 연결을 요청한다.
- 연결 수락 및 데이터 교환
- 서버는 클라이언트 요청을 수락하고, Socket을 통해 데이터를 송수신한다.
- 연결 종료
- 데이터 전송이 끝나면 클라이언트와 서버 모두 연결을 종료한다.
- 서버 생성
Echo 프로그래밍 - TCP 편
💡
클라이언트가 서버에 메시지를 보내면, 서버가 동일한 메시지를 클라이언트에게 되돌려주는 간단한 네트워크 애플리케이션
- 실행 방법
- EchoServer 클래스를 실행하여 서버를 시작
- EchoClient 클래스를 실행하여 클라이언트를 서버에 연결
- 클라이언트에서 메시지를 입력하고 엔터를 누르면, 서버가 동일한 메시지를 되돌려준다.
단일 클라이언트를 받아들이는 EchoServer

[ Echo Server ]
- ServerSocket 객체 생성
- 특정 포트에서 클라이언트의 연결 요청을 기다리기 위한 ServerSocket 객체 생성
// 포트 번호 9999를 사용하여 새로운 서버 소켓 생성 // 클라이언트의 연결 요청을 수신하고, 해당 포트 번호를 통해 서버에 연결할 수 있게 된다. ServerSocket server = new ServerSocket(9999);
- 클라이언트 연결 수락
- 클라이언트 연결 요청이 들어오면, accept()를 통해 연결을 수락하고 Socket 객체 생성
// 서버 소켓에서 클라이언트의 연결을 수락하고, 클라이언트와 통신하기 위한 소켓을 생성 Socket sc = server.accept();
- 데이터 읽고 쓰기
- 클라이언트로부터 데이터를 읽기 위해 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(); // 버퍼를 비워서 즉서 전송 }
- 연결 종료
- 클라이언트와의 통신이 끝나면, Socket을 닫아 연결을 종료
- 클라이언스 소켓 : 서버에서 해당 클라이언트로 더 이상 통신이 불가능
- 서버 소켓 : 서버가 더 이상 클라이언트의 연결을 받지 않을 것이라면, 서버 소켓도 단는다. (더 이상 클라이언트의 연결 요청을 수락하지 않음)
server.close(); sc.close(); br.close();; pw.close();
- 클라이언트와의 통신이 끝나면, Socket을 닫아 연결을 종료
전체 코드
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 ]
- Socket 객체 생성
- 서버의 IP 주소와 포트 번호를 사용하여 Socket 객체를 생성하여 서버에 연결
// 새로운 클라이언트 소켓을 생성한다. // 이 소켓은 127.0.0.1 주소와 포트 번호 9999로 지정된 서버에 연결된다. Socket sock = new Socket("127.0.0.1", 9999);
- 서버의 IP 주소와 포트 번호를 사용하여 Socket 객체를 생성하여 서버에 연결
- 데이터 전송 및 응답 수신
- 서버로부터의 응답을 받기 위해 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); }
- 연결 종료
- 데이터 송수신이 끝나면 Socket을 닫아 연결을 종료
pw.close(); br.close(); keybord.close(); sock.close();
- 데이터 송수신이 끝나면 Socket을 닫아 연결을 종료
전체 코드
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를 이용하면 된다!
- 구현 방법
- 클라이언트 연결을 수락하기 위한 ServerScoket 생성
- 클라이언트가 연결될 때마다 새로운 스레드를 생성하여 해당 클라이언트의 요청을 처리
- 각 클라이언트 스레드는 해당 클라이언트와 통신하고, 클라이언트로부터 받은 데이터를 다른 모든 클라이언트에게 다시 전송
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 ]
- ServerSocket 설정
- ServerSocket을 이용하여 특정 포트에 클라이언트의 연결을 기다린다.
- 클라이언트 연결 처리
- 클라이언트 연결 요청이 들어오면 accept() 메서드를 통해 이를 수락한다.
- 각 클라이언트마다 별도의 스레드를 생성하여 독립적으로 처리한다.
- 메시지 중계
- 서버는 클라이언트로부터 받은 메시지를 다른 클라이언트에게 전달(브로드캐스트)
// 전체 사용자에게 메시지 보내기 메소드 (브로드캐스트) 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 ]
- Socket을 이용한 서버 연결
- 클라이언트는 서버의 IP 주소와 포트 번호를 사용하여 서버에 연결한다,
- 메시지 송수신
- 콘솔에서 입력받은 메시지를 서버에 보내고, 서버로부터 오는 메시지를 콘솔에 출력한다.
전체 코드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