Ryuzy 2025. 5. 28. 17:14
728x90
반응형

1. 소켓

소켓(Socket)은 네트워크를 통해 두 컴퓨터가 데이터를 주고받기 위한 통신의 출입구(Endpoint) 역할을 하는 소프트웨어 인터페이스입니다. 서버와 클라이언트가 소켓을 통해 연결되면, 마치 전화선을 통해 대화하듯이 양방향으로 데이터를 주고받을 수 있는 통신 통로가 형성됩니다. 자바나 파이썬 등 여러 프로그래밍 언어에서는 소켓 라이브러리를 제공하며, 이를 활용하면 TCP/IP 또는 UDP 방식의 네트워크 프로그램을 구현할 수 있습니다. 예를 들어 채팅 프로그램, 게임 서버, 실시간 데이터 송수신 시스템 등이 모두 소켓을 기반으로 동작합니다.

 

tcp/ip

TCP/IP는 인터넷을 포함한 대부분의 네트워크에서 사용되는 표준 통신 프로토콜 체계로, 데이터를 정확하고 안정적으로 주고받기 위한 규칙들의 집합입니다. TCP(Transmission Control Protocol)는 데이터의 신뢰성과 순서를 보장하며, IP(Internet Protocol)는 데이터를 목적지까지 전달하는 역할을 합니다. TCP는 데이터를 작은 조각(패킷)으로 나누고, 수신자가 이를 정확하게 재조립하도록 도와주며, 전송 중 손실되면 재전송을 요청합니다. IP는 이러한 패킷들이 인터넷을 통해 올바른 주소로 전달되도록 합니다. 두 프로토콜이 함께 동작하면서, 웹 브라우징, 이메일, 파일 전송 등 인터넷 기반 서비스가 가능해집니다.

 

UDP

UDP(User Datagram Protocol)는 TCP와 달리 데이터의 전송 순서나 도착 여부를 보장하지 않는 비연결형 통신 프로토콜입니다. 빠른 속도가 중요한 실시간 통신에서 주로 사용되며, TCP보다 구조가 간단하고 오버헤드가 적어 전송 지연이 매우 짧습니다. 하지만 오류 검사나 재전송 같은 기능이 없기 때문에 데이터가 유실되거나 순서가 바뀔 수도 있습니다. 대표적인 사용 사례로는 실시간 영상 스트리밍, 온라인 게임, 음성 통화(VoIP) 등이 있으며, 이들 서비스는 약간의 데이터 손실보다 빠른 전송을 더 중요하게 생각합니다.

 

 

2. 소켓 통신

자바의 소켓 통신은 Socket 클래스와 ServerSocket 클래스를 이용하여 클라이언트와 서버가 네트워크를 통해 데이터를 주고받을 수 있도록 해줍니다. Java에서는 기본적으로 TCP 기반의 소켓 통신을 제공합니다.

 

1. 서버

  • ServerSocket을 열고 특정 포트에서 클라이언트의 연결 요청을 대기함.
  • accept() 메서드로 클라이언트가 접속하면 Socket 객체를 생성하여 통신을 시작함.

 

2. 클라이언트

  • Socket을 사용하여 서버의 IP와 포트를 통해 서버에 연결 요청을 보냄.
  • 연결이 성공되면 서버와 데이터를 주고받을 수 있음.
  • 서버/클라이언트 양쪽 모두 InputStream과 OutputStream을 이용해 문자열, 숫자 등의 데이터를 송수신함.
import java.io.*;
import java.net.*;

public class Server {
    public static void main(String[] args) {
        int port = 12345;

        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("서버 대기 중...");

            Socket clientSocket = serverSocket.accept(); // 클라이언트 접속 대기
            System.out.println("클라이언트 연결됨: " + clientSocket.getInetAddress());

            // 데이터 입출력 스트림
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);

            // 클라이언트로부터 메시지 수신
            String input = in.readLine();
            System.out.println("클라이언트로부터 받은 메시지: " + input);

            // 응답 메시지 전송
            out.println("서버에서 받은 메시지: " + input);

            clientSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

import java.io.*;
import java.net.*;

public class Client {
    public static void main(String[] args) {
        String serverIP = "127.0.0.1"; // 로컬호스트
        int port = 12345;

        try (Socket socket = new Socket(serverIP, port)) {
            System.out.println("서버에 연결됨");

            // 데이터 입출력 스트림
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

            // 서버로 메시지 전송
            out.println("안녕하세요, 서버!");

            // 서버 응답 받기
            String response = in.readLine();
            System.out.println("서버 응답: " + response);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

 

3. 다중 클라이언트 채팅

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;

public class ChatServer {
    private static final int PORT = 12345;
    private static final Set<ClientHandler> clients = ConcurrentHashMap.newKeySet();

    public static void main(String[] args) {
        System.out.println("채팅 서버 시작...");

        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            while (true) {
                Socket socket = serverSocket.accept();
                System.out.println("클라이언트 연결됨: " + socket.getInetAddress());

                ClientHandler handler = new ClientHandler(socket);
                clients.add(handler);
                new Thread(handler).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 모든 클라이언트에게 메시지 전달
    public static void broadcast(String message, ClientHandler sender) {
        for (ClientHandler client : clients) {
            if (client != sender) {
                client.sendMessage(message);
            }
        }
    }

    // 클라이언트 종료 시 리스트에서 제거
    public static void removeClient(ClientHandler client) {
        clients.remove(client);
    }
}

 

import java.io.*;
import java.net.*;

public class ClientHandler implements Runnable {
    private Socket socket;
    private BufferedReader in;
    private PrintWriter out;

    public ClientHandler(Socket socket) {
        this.socket = socket;
        try {
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            out.println("서버에 연결되었습니다. 닉네임을 입력하세요:");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void sendMessage(String message) {
        out.println(message);
    }

    @Override
    public void run() {
        try {
            String name = in.readLine();
            ChatServer.broadcast(name + "님이 입장하셨습니다.", this);

            String message;
            while ((message = in.readLine()) != null) {
                if (message.equalsIgnoreCase("exit")) break;
                ChatServer.broadcast(name + ": " + message, this);
            }

            out.println("채팅을 종료합니다.");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                ChatServer.removeClient(this);
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 

import java.io.*;
import java.net.*;

public class ChatClient {
    public static void main(String[] args) {
        String serverIP = "127.0.0.1";
        int port = 12345;

        try (Socket socket = new Socket(serverIP, port)) {
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

            // 메시지 수신을 별도 스레드에서 처리
            new Thread(() -> {
                String serverMsg;
                try {
                    while ((serverMsg = in.readLine()) != null) {
                        System.out.println(serverMsg);
                    }
                } catch (IOException e) {
                    System.out.println("서버 연결 종료");
                }
            }).start();

            // 키보드 입력 → 서버로 전송
            String input;
            while ((input = keyboard.readLine()) != null) {
                out.println(input);
                if (input.equalsIgnoreCase("exit")) break;
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

 

4. 파일 전송

1. 바이너리 데이터 전송

  • 문자열 전송과 달리 파일은 바이너리 데이터(0과 1)입니다.
  • 텍스트 전송에 쓰이는 BufferedReader, PrintWriter는 텍스트 전용 스트림이므로 파일 전송에는 InputStream과 OutputStream을 사용해야 합니다.

 

2. 파일 크기 및 이름 전달

  • 파일 전송 전에는 수신자가 파일의 이름, 크기 등을 알아야 저장할 수 있습니다.
  • 따라서 파일 전송은 일반적으로 다음과 같은 순서로 진행됩니다.
1. 파일 전송 명령 전송 (예: /file filename.jpg)
2. 서버가 수신 준비
3. 클라이언트가 파일 크기와 이름을 먼저 보냄
4. 이후 실제 파일 바이너리 전송

 

3. 스레드 분리 또는 프로토콜 구분

  • 기존 채팅 메시지와 파일 전송 데이터를 구분하기 위한 간단한 프로토콜(명령 구분자)이 필요합니다.
  • 예: 채팅 메시지는 그냥 보내고, 파일은 /file filename 명령으로 구분

 

다중 클라이언트 채팅 (파일 전송 기능 추가)

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;

public class ChatServer {
    private static final int PORT = 12345;
    private static final Set<ClientHandler> clients = ConcurrentHashMap.newKeySet();

    public static void main(String[] args) {
        System.out.println("채팅 서버 시작...");

        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            while (true) {
                Socket socket = serverSocket.accept();
                ClientHandler handler = new ClientHandler(socket);
                clients.add(handler);
                new Thread(handler).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void broadcast(String message, ClientHandler sender) {
        for (ClientHandler client : clients) {
            if (client != sender) {
                client.sendMessage(message);
            }
        }
    }

    public static void removeClient(ClientHandler client) {
        clients.remove(client);
    }
}

 

import java.io.*;
import java.net.*;

public class ClientHandler implements Runnable {
    private Socket socket;
    private BufferedReader in;
    private PrintWriter out;

    public ClientHandler(Socket socket) {
        this.socket = socket;
        try {
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(), true);
            out.println("서버에 연결되었습니다. 닉네임을 입력하세요:");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void sendMessage(String message) {
        out.println(message);
    }

    private File getUniqueFile(String baseName) {
        File file = new File("server_" + baseName);
        int count = 1;
        while (file.exists()) {
            String name = baseName;
            int dotIndex = name.lastIndexOf('.');
            if (dotIndex != -1) {
                name = name.substring(0, dotIndex) + "(" + count + ")" + name.substring(dotIndex);
            } else {
                name = name + "(" + count + ")";
            }
            file = new File("server_" + name);
            count++;
        }
        return file;
    }

    private void receiveFile(String fileName) {
        try {
            DataInputStream dis = new DataInputStream(socket.getInputStream());

            long fileSize = dis.readLong();
            File file = getUniqueFile(fileName);
            FileOutputStream fos = new FileOutputStream(file);

            byte[] buffer = new byte[4096];
            long totalRead = 0;
            int read, percent = 0;

            System.out.println("파일 수신 시작: " + file.getName() + " (" + fileSize + " bytes)");

            while (totalRead < fileSize && (read = dis.read(buffer)) != -1) {
                fos.write(buffer, 0, read);
                totalRead += read;
                int newPercent = (int)((totalRead * 100) / fileSize);
                if (newPercent > percent) {
                    percent = newPercent;
                    System.out.print("\r서버 수신 진행률: " + percent + "%");
                }
            }

            System.out.println("\n파일 저장 완료: " + file.getName());
            fos.close();
            ChatServer.broadcast("파일 [" + file.getName() + "] 수신 완료", this);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            String name = in.readLine();
            ChatServer.broadcast(name + "님이 입장하셨습니다.", this);

            String message;
            while ((message = in.readLine()) != null) {
                if (message.startsWith("/file ")) {
                    String fileName = message.substring(6);
                    receiveFile(fileName);
                } else if (message.equalsIgnoreCase("exit")) {
                    break;
                } else {
                    ChatServer.broadcast(name + ": " + message, this);
                }
            }

            out.println("채팅을 종료합니다.");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                ChatServer.removeClient(this);
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

 

import java.io.*;
import java.net.*;

public class ChatClient {
    public static void main(String[] args) {
        String serverIP = "127.0.0.1";
        int port = 12345;

        try (Socket socket = new Socket(serverIP, port)) {
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            BufferedReader keyboard = new BufferedReader(new InputStreamReader(System.in));
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

            // 서버 메시지 수신 스레드
            new Thread(() -> {
                try {
                    String line;
                    while ((line = in.readLine()) != null) {
                        System.out.println(line);

                        if (line.startsWith("파일 [") && line.endsWith("] 수신 완료")) {
                            String fileName = line.substring(4, line.indexOf("]"));
                            receiveFile(socket, fileName);
                        }
                    }
                } catch (IOException e) {
                    System.out.println("서버 연결 종료");
                }
            }).start();

            // 키보드 입력 처리
            String input;
            while ((input = keyboard.readLine()) != null) {
                if (input.startsWith("/file ")) {
                    String filePath = input.substring(6);
                    sendFile(socket, filePath, out);
                } else {
                    out.println(input);
                    if (input.equalsIgnoreCase("exit")) break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void sendFile(Socket socket, String filePath, PrintWriter out) {
        try {
            File file = new File(filePath);
            long fileSize = file.length();
            String fileName = file.getName();

            out.println("/file " + fileName);

            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
            FileInputStream fis = new FileInputStream(file);

            dos.writeLong(fileSize);

            byte[] buffer = new byte[4096];
            long totalSent = 0;
            int read, percent = 0;

            System.out.println("파일 전송 시작: " + fileName + " (" + fileSize + " bytes)");

            while ((read = fis.read(buffer)) != -1) {
                dos.write(buffer, 0, read);
                totalSent += read;
                int newPercent = (int)((totalSent * 100) / fileSize);
                if (newPercent > percent) {
                    percent = newPercent;
                    System.out.print("\r클라이언트 전송 진행률: " + percent + "%");
                }
            }

            fis.close();
            dos.flush();
            System.out.println("\n파일 전송 완료: " + fileName);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static File getUniqueFile(String baseName) {
        File file = new File("client_" + baseName);
        int count = 1;
        while (file.exists()) {
            String name = baseName;
            int dotIndex = name.lastIndexOf('.');
            if (dotIndex != -1) {
                name = name.substring(0, dotIndex) + "(" + count + ")" + name.substring(dotIndex);
            } else {
                name = name + "(" + count + ")";
            }
            file = new File("client_" + name);
            count++;
        }
        return file;
    }

    private static void receiveFile(Socket socket, String fileName) {
        try {
            DataInputStream dis = new DataInputStream(socket.getInputStream());
            long fileSize = dis.readLong();
            File file = getUniqueFile(fileName);
            FileOutputStream fos = new FileOutputStream(file);

            byte[] buffer = new byte[4096];
            long totalRead = 0;
            int read, percent = 0;

            System.out.println("파일 수신 시작: " + file.getName() + " (" + fileSize + " bytes)");

            while (totalRead < fileSize && (read = dis.read(buffer)) != -1) {
                fos.write(buffer, 0, read);
                totalRead += read;
                int newPercent = (int)((totalRead * 100) / fileSize);
                if (newPercent > percent) {
                    percent = newPercent;
                    System.out.print("\r클라이언트 수신 진행률: " + percent + "%");
                }
            }

            fos.close();
            System.out.println("\n파일 저장 완료: " + file.getName());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
728x90
반응형