본문 바로가기

프로그래밍/Java

자바 TCP/IP 소켓 통신 서버, 클라이언트 만들기 코드

728x90
반응형

일이 있어서 자바 TCP/IP 소켓 통신 방식에 대해 잠시 공부를 했다

전에 파이썬 플라스크로 HTTP 서버를 제작했었는데

이번에는 자바의 'java.net' 패키지에서 제공하는 Socket 클래스를 이용해서 만드는 통신 서버를 만들어봤다

 

몇 달전에 네트워크관리사 2급 공부를 하면서 TCP랑 UDP의 차이 그리고 동기, 비동기 방식의 차이를

달달 외우기 까지 했었는데 이제와서 기억나지 않는걸 보니 내가 치매거나 제대로 공부하지 않았거나겠네

그래도 네트워크관리사2급 필기는 합격했다 ㅎㅎ

 

 

Java에서 소켓 통신은 클라이언트와 서버 간의 네트워크 통신을 가능하게 하는 API입니다. Java에서 소켓 통신은 java.net 패키지에서 제공됩니다.

 

소켓 통신은 일반적으로 클라이언트와 서버 사이의 양방향 통신을 지원하며, TCP/IP 프로토콜을 기반으로 작동합니다. TCP/IP 프로토콜은 인터넷에서 가장 널리 사용되는 프로토콜 중 하나이며, 네트워크 통신에서 신뢰성과 안정성을 보장합니다.

 

Java에서 소켓 통신을 구현하려면, 클라이언트와 서버 모두가 소켓을 생성해야 합니다. 클라이언트는 서버의 IP 주소와 포트 번호를 사용하여 소켓을 생성하고, 서버는 클라이언트의 연결 요청을 수락하기 위해 서버 소켓을 생성합니다.

클라이언트와 서버는 소켓을 통해 데이터를 주고받을 수 있습니다. 소켓은 스트림을 통해 데이터를 전송하며, 클라이언트는 소켓 스트림에서 데이터를 읽고, 서버는 소켓 스트림에 데이터를 쓰는 방식으로 통신합니다.

 

Java에서는 Socket 클래스를 사용하여 클라이언트와 서버 간의 소켓 통신을 구현합니다. Socket 클래스는 클라이언트 측에서 서버에 연결되는 소켓을 생성하고, ServerSocket 클래스는 서버 측에서 클라이언트의 연결 요청을 수락하는 소켓을 생성합니다.

 

Socket 클래스와 ServerSocket 클래스를 사용하여 소켓 통신을 구현할 때, InputStream과 OutputStream 클래스를 사용하여 데이터를 전송합니다. 클라이언트에서는 소켓으로부터 InputStream을 얻어서 데이터를 읽고, 서버에서는 소켓으로부터 OutputStream을 얻어서 데이터를 씁니다. 이러한 방식으로 클라이언트와 서버 간의 데이터 통신을 구현할 수 있습니다.

 

자바 Socket에 대한 정보를 복붙했다

어차피 인터넷에 더 좋은 글들이 많고 지금은 챗gpt에게 물어보면 다 답해준다

 

나의 서버 코드는 대충 인원수를 50명으로 잡고 그 이상 접속하지 못하게 한다

서버의 클라이언트 방이라고 하는 클라이언트 배열은 차곡차곡 클라이언트가 접속할 수 있도록 구조를 짰으며

클라이언트가 접속을 끊고 다시 접속하면 다음 방으로 들어간다

방의 끝으로 도달하면 다시 처음 방으로 들어가서 접속할 수 있도록한다

또한, 서버는 예기치 못한 오류가 발생할 수 있다. try catch를 떡칠해서 최대한 오류가 나지 않도록 만들었다

 

클라이언트는 서버와 접속이 끊겨도 일정 주기로 서버와 연결하려고 하며 연결되면 재접속했다는 알림을 표시한다

 

서버와 클라이언트 모두 양방향 통신이 가능하며 메시지를 보내고 받을 수 있다.

클라이언트가 보내는 메시지는 서버에서 볼 수 있고

서버에서 메시지를 보내면 접속중인 클라이언트 모두 메시지를 볼 수 있다.

 

특정 클라이언트만 메시지를 보내려면 인덱스를 매개변수로 받고 메시지를 보내는 메소드를 따로 만들면 된다

 

어차피 다른 코드나 틀은 비슷하니까 서버, 클라이언트를 제작하려면 복붙해서 수정해서 쓰는게 편하다

 

https://zynar.tistory.com/225

 

[자바 JAVA] Server Socket 소켓 통신 한글 깨짐 해결 BufferedReader, InputStreamReader, PrintWriter, OutputStreamWrite

보통 자바 소켓 통신에서 한글이 깨져서 출력될 때는 코드에 인코딩이 제대로 안되어 있으면 한글이 깨질 가능성이 높다고 한다. 그래서 서버와 클라이언트 모두 UTF-8 인코딩 설정을 해줘야하지

zynar.tistory.com

 

그리고 통신을 하면 한글이 깨질 때가 있는데 utf-8 인코딩 문제거나 jdk 문제일 확률이 크다

위에 내가 썼던 방식을 정리해놨다

 


서버 코드

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

import static java.lang.System.exit;

public class TCPServer {
    private static final int SERVER_PORT = 12345; // 서버 포트번호
    private static final int SERVER_SPACE = 50; // 서버에서 가용할 수 있는 클라이언트 수 (임의 변경)
    private static final Socket[] clientSockets = new Socket[SERVER_SPACE];    // 클라이언트 소켓 배열
    private static ServerSocket serverSocket;   // 서버 소켓

    public static void main(String[] args) throws Exception {

        // 서버 소켓 생성
        serverSocket = new ServerSocket(SERVER_PORT);
        System.out.println("TCP Server started.");

        // 서버 오픈
        // 서버는 항상 루프가 돌고 있어야 하기 때문에 쓰레드로 구현
        // 나머지도 마찬가지
        Thread serverAcceptThread = new Thread(() -> {
            int i = 0;
            while (true) {
                try {
                    // 클라이언트 대기
                    System.out.println("Waiting for clients...");
                    // 클라이언트가 접속할때가지 아래 코드에서 기다린다
                    // 클라이언트가 접속하지 않으면 코드가 더이상 진행되지 않는다
                    clientSockets[i] = serverSocket.accept();
                    System.out.println("Client connected: " + clientSockets[i].getInetAddress().getHostAddress());

                    if (clientSockets[i] != null && clientSockets[i].isConnected()) {
                        // 클라이언트에서 서버로 메시지를 전송받는 클래스 생성
                        ClientToServerThread clientHandler = new ClientToServerThread(i);
                        clientHandler.start();
                    }

                    // 클라이언트 최대 수 만큼 인덱스가 자동 증가하고 넘어가는 숫자는 0부터 시작
                    // 이렇게 하면 클라이언트가 서버의 남는 소켓이 없도록 차곡차곡 접속할 수 있다
                    i = (i + 1) % clientSockets.length;
                } catch (IOException e) {
                    serverSocket = null;
                    break;
                }
            }

        });
        serverAcceptThread.start();

        // 서버에서 클라이언트로 메시지를 보내는 클래스 생성
        ServerToClientThread serverToClientThread = new ServerToClientThread();
        serverToClientThread.start();
    }

    static class ServerToClientThread extends Thread {
        @Override
        public void run() {
            super.run();
            try {
                while(true) {
                    BufferedReader br = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
                    String input = br.readLine();

                    // 서버를 종료하고 싶을 때
                    if(input.equals("exit")) {
                        System.out.println("Server off... Goodbye!");
                        for (Socket socket : clientSockets) {
                            if (socket!=null) {
                                socket.close();
                            }
                        }
                        serverSocket.close();
                        break;
                    }

                    // 소켓 배열의 접속되어 있는 클라이언트를 찾아서 차례대로 전송한다
                    for (Socket clientSocket : clientSockets) {
                        if (clientSocket!= null && clientSocket.isConnected()) {
                            OutputStream out = clientSocket.getOutputStream();
                            PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8), true);
                            writer.println(input);
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    static class ClientToServerThread extends Thread {
        private final int index;
        private final InputStream in;
        private final BufferedReader reader;

        ClientToServerThread(int index) throws IOException {
            this.index = index;
            this.in = clientSockets[index].getInputStream();
            this.reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
        }

        @Override
        public void run() {
            super.run();
            while (true) {
                // 클라이언트로부터 메시지 수신
                try {
                    String message = reader.readLine();
                    System.out.println("Message from client: " + message);
                } catch (IOException e) {
                    System.out.printf("Client[%d] disconnected.\n", index);
                    clientSockets[index] = null;
                    break;
                }
            }
        }
    }
}

 

클라이언트 코드

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

import static java.lang.Thread.sleep;

public class TCPClient {
    private static boolean isRunning;
    private static Socket clientSocket = null;
    private static final int SERVER_PORT = 12345;
    public static void main(String[] args) {
        String serverHostname = "127.0.0.1"; // 서버 IP 주소
        isRunning = true;
        // 서버에 연결하는 쓰레드
        // 서버에 연결 중인 클라이언트는 한 번만 연결하면 해당 쓰레드는 필요 없지만
        // 서버가 끊기고 다시 서버에 자동을 접속하려면 루프가 필요하다
        Thread serverThread = new Thread(() -> {
            // 서로 다른 쓰레드에서 동작한다
            ServerToClientThread serverToClientThread = null;    // 서버에서 클라이언트로 메시지를 받는 쓰레드
            ClientToServerThread clientToServerThread = null;    // 클라이언트에서 서버로 메시지를 보내는 쓰레드

            while (isRunning) {
                if (clientSocket == null) {
                    try {
                        System.out.println("Server finding...");
                        clientSocket = new Socket(serverHostname, SERVER_PORT);
                        if (clientSocket.isConnected()) {
                            System.out.println("Connected to server.");
                            // 서버와 접속하면 소켓을 사용하는 쓰레드 생성
                            serverToClientThread = new ServerToClientThread();
                            serverToClientThread.start();
                            clientToServerThread = new ClientToServerThread();
                            clientToServerThread.start();
                        }
                    } catch (IOException e) {
                        System.out.println("Failed to connect to server. Retrying...");
                        // 서버와 연결이 끊기면 현재 접속중인 소켓을 사용중인 쓰레드를 전부 종료한다
                        if (serverToClientThread != null) {
                            serverToClientThread.setFlag(false);
                        }
                        if (clientToServerThread!= null) {
                            clientToServerThread.setFlag(false);
                        }
                    }
                }
                try {
                    // 재접속은 1초 간격으로 한다
                    Thread.sleep(1000);
                } catch (InterruptedException ignored) {
                }
            }
        });
        serverThread.start();

    }

    static class ClientToServerThread extends Thread {
        private boolean flag = true;
        private final OutputStream out;
        private final BufferedReader br;

        ClientToServerThread() throws IOException {
            out = clientSocket.getOutputStream();
            br = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
        }

        @Override
        public void run() {
            super.run();
            // 서버로 메시지 전송
            while (flag) {
                try {
                    // 서버로 보낼 메시지를 받는 코드
                    // 애플리케이션 자체를 종료하려면 해당 명령어 입력
                    String input = br.readLine();
                    if(input.equals("exit")) {
                        System.out.println("Client Off... Goodbye!");
                        clientSocket = null;
                        isRunning = false;
                        break;
                    }
                    PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8), true);
                    writer.println(input);
                } catch (IOException e) {
                    System.out.println("Failed to send message.");
                }
            }
        }

        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    }

    static class ServerToClientThread extends Thread {
        private boolean flag = true;
        private final BufferedReader reader;

        public ServerToClientThread() throws IOException {
            InputStream in = clientSocket.getInputStream();
            reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
        }

        @Override
        public void run() {
            super.run();
            // 서버로부터 응답 수신
            while (flag) {
                try {
                    String response = reader.readLine();
                    System.out.println("Response from server: " + response);
                } catch (IOException e) {
                    // 서버와 연결이 끊기면 현재 사용중인 소켓은 null 으로 변경하여
                    // 서버 접속 쓰레드에서 재접속 하도록 유도
                    System.out.println("Disconnected from server.");
                    clientSocket = null;
                    break;
                }
            }
        }

        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    }


}

https://github.com/wndudwkd003/TCP-Client-Server-Study

 

GitHub - wndudwkd003/TCP-Client-Server-Study

Contribute to wndudwkd003/TCP-Client-Server-Study development by creating an account on GitHub.

github.com

 

728x90
반응형