[JAVA] 자바 소켓 통신 이론 및 코드

용용이·2023년 5월 22일
0

JAVA

목록 보기
1/2

Java 소켓 통신(Socket)을 사용하는 이유

먼저 자바에서 소켓 통신은 C 또는 C++ 언어로 구현된 프로젝트와의 통신에 많이 사용됩니다.

이유는 Java와 C의 데이터 개념이 다르기 때문인데요. C에서는 구조체를 사용하는데 반해서 Java에는 구조체가 없습니다.

이처럼 Java의 Object 구조를 C에서 이해하지 못하고 C의 구조체를 자바에서 이해하지 못하기 때문에 서로 통신을 위해서는 byte 단위로 정보를 주고받아야 합니다.

(Socket을 사용한 전문 통신)

Http 통신과 Socket 통신의 차이점

- 단방향 통신인 Http 통신

Http 통신은 Client의 요청(Request)이 있을 때만 서버가 응답(Response)하여 해당 정보를 전송하고 곧바로 연결을 종료하는 방식입니다. Client가 요청을 보내는 경우에만 Server가 응답하는 단방향 통신으로 반대로 Server가 Client에게 요청을 보낼 수는 없습니다.

- 양방향 통신인 Socket 통신

Server와 Client가 특정 Port를 통해 실시간으로 양방향 통신을 하는 방식입니다. Http 통신과는 다르게 Server와 Client가 특정 Port를 통해 연결되어 있어서 실시간으로 양뱡향 통신을 할 수 있습니다.

Streaming 중계나 실시간 채팅, 게임 등과 같이 즉각적으로 정보를 주고받는 경우에 사용됩니다.

Stream이란? (InputStream, OutputStream)

아래 Socket 통신 예제 코드에서 보게 될 Stream에 대해서 간단하게 이야기하고 넘어가겠습니다.

Stream은 프로그램 동작 중 외부에서 데이터를 읽거나 외부로 데이터를 출력하는 작업에 사용됩니다. 이때 데이터는 어떤 통로를 통해서 이동되는데 이 통로를 Stream이라고 합니다.

자바에서는 외부에서 데이터를 읽는 역할을 수행하는 InputStream과 외부로 데이터를 출력하는 역할을 수행하는 OutputStream이 존재하며, 이 둘은 단일 방향으로 연속적으로 흘러갑니다.

(단방향이라는 특징 때문에 하나의 스트림으로 입출력을 동시에 할 수 없어서 InputStream과 OutputStream이 따로 존재합니다.)

Socket 통신 흐름 살펴보기

소켓은 응용프로그램에서 TCP/IP를 이용하는 창구 역할을 하며, 두 프로그램이 네트워크를 통해 서로 통신을 수행할 수 있도록 양쪽에서 생성되는 링크의 단자입니다. 두 소켓이 연결되면 서로 다른 프로그램이 서로 데이터를 전달할 수 있게 됩니다.

이러한 Socket 통신은 일련의 규칙이 정해져 있는데요.

  1. 먼저 기다리는 측을 Server라고 하며, Server에서는 Port를 열고 Client의 접속을 기다립니다.
  2. 그리고 접속하는 측을 Client라고 하며, Server의 IP와 Port에 접속하여 통신이 연결됩니다.
  3. Server와 Client 간의 통신은 Send, Receive의 형태로 주고받습니다.
  4. 그리고 통신이 끝나면 close()로 접속을 끊습니다.



구현 코드 (Server)


/*
 Chatting 서버
 1. 클라이언트의 접속 -> ServerWorker Thread 생성 및 start
 2. ServerWorker -> 개별 client에 채팅 서비스

 */
public class ChatServer {
    private ArrayList<ServerWorker> list = new ArrayList<ServerWorker>();

    public void go() throws IOException {
        ServerSocket serverSocket = null;
        try {

            // 채팅 서버 시작
            serverSocket = new ServerSocket(5432);
            System.out.println("**ChatServer Start**");

            // 다수의 클라이언트에게 지속적으로 서비스하기 위해 while 이용
            while (true) {
                Socket socket = serverSocket.accept();
                ServerWorker sw = new ServerWorker(socket);
                list.add(sw);
                Thread thread = new Thread(sw);
                thread.start();
            }

        } finally {
            if (serverSocket != null)
                serverSocket.close();
            System.out.println("**ChatServer End**");
        }
    }

    public void sendMessage(String message) {
        System.out.println(message);
        // 접속해 있는 모든 클라이언트들에게 메세지 전송
        for (int i=0;i<list.size();i++) {
            list.get(i).pw.println(message);
        }
    }

    class ServerWorker implements Runnable {
        private Socket socket;
        private BufferedReader br;
        private PrintWriter pw;
        private String user;

        public ServerWorker(Socket socket) {
            super();
            this.socket = socket;
            user = socket.getInetAddress().toString().replaceAll("/","");
        }
        public void chatting() throws IOException {
            br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            pw = new PrintWriter(socket.getOutputStream(),true);
            sendMessage(user+"님이 입장하셨습니다");

            //로그 생성 영역 시작
            LocalDateTime currentDateTime = LocalDateTime.now();
            String filePath = "Test.txt";

            File file = new File(filePath); // File객체 생성
            if(!file.exists()){ // 파일이 존재하지 않으면
                file.createNewFile(); // 신규생성
            }

            // BufferedWriter 생성
            BufferedWriter writer = new BufferedWriter(new FileWriter(file, true));

            //로그 생성 영역 끝

            try {
                while (true) {
                    String message = br.readLine();
                    if (message.trim().equals("종료") || message.equals("null") || message == null) {

                        break;
                    }
                    sendMessage(user+"님:"+message);

                    //여기다 로그 남기기
                    writer.write("[" + currentDateTime +"] " + "[" + user + "] " + "[" + message + "]");
                    writer.newLine();
                    writer.flush();

                } // while
            } // echo method
            catch (Exception e) {
            }
        }

        public void run() {
            try {
                chatting();

            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    closeAll();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                sendMessage(user+"님이 나가셨습니다!!");
                list.remove(this);
            }
        }

        public void closeAll() throws IOException {
            if (pw != null)
                pw.close();
            if (br != null)
                br.close();
            if (socket != null)
                socket.close();
        }

    }

    public static void main(String[] args) {
        try {
            new ChatServer().go();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Server
데이터 송수신은 blocking 방식으로 동작합니다. 때문에 메인 스레드로만 구성하면 데이터를 올바르게 수신하는데 문제가 생길 수 있어 Multi-Thread 서버로 구현하였습니다.

* Thread 클래스는 start() 메서드 실행 시 run() 메서드가 수행되도록 내부적으로 동작합니다.



Server 코드에서는 'ServerSocket'과 'Socket' 두 가지 소켓을 볼 수 있는데요.

서버 소켓은 말 그대로 서버 프로그램에서 사용하는 소켓으로 ServerSocket 객체를 생성하여 클라이언트가 연결해오는 것을 기다립니다.

클라이언트가 연결해 올 때마다 요청은 요청 큐(Request Queue)에 쌓이고, 각각의 클라이언트 연결에 accept() 함으로써 요청을 요청 큐에서 꺼내고 Socket 객체가 리턴됩니다.

이렇게 리턴되는 Socket을 활용하여 클라이언트와 데이터를 주고받으며, 예시와 같은 멀티스레드(Multi-Thread) 환경에서는 Socket을 생성한 스레드에 주어서 클라이언트와 데이터를 주고받습니다.


Socket socket = ServerSocket.accept();

ServerSocket에서 내부적으로 동작하는 코드를 살펴보면 accept()를 통해 Socket 객체를 가지고 오는 부분에서 내부적으로 bind -> listen 과정이 실행됩니다.

구현 코드 (Client)


/*
채팅 클라이언트
스레드 ( 세부적 실행단위 )
1. ReceiverWorker implements Runnable : 친구들의 메세지를 입력받는 역할
2. ChatClient의 main thread : ReceiverWorker Thread 생성 start
	자신은 친구들에게 메세지를 출력하는 역할
 */
public class ChatClient {
    private Socket socket;
    private BufferedReader br;
    private PrintWriter pw;
    private Scanner sc;

    public void go() throws UnknownHostException, IOException {
        try {
            socket = new Socket("127.0.0.1", 5432);
            br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            pw = new PrintWriter(socket.getOutputStream(),true);
            sc = new Scanner(System.in);

            ReceiverWorker rw = new ReceiverWorker();
            Thread thread = new Thread(rw);
            thread.setDaemon(true);

            thread.start();

            System.out.println("**ChatClient가 서버에 접속**");

            while (true) {

                //System.out.print("서버에 보낼 메세지:");
                String message = sc.nextLine();
                pw.println(message);

                if (message.trim().equals("종료")) {
                    System.out.println("**ChatClient 종료합니다**");
                    break;
                }
            }
        }finally {
            closeAll();
        }
    }

    class ReceiverWorker implements Runnable {

        public void run() {
            try {

                receiveMessage();

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

        }

        public void receiveMessage() throws IOException {
            while (true) {
                String message = br.readLine();

                if (message == null) {
                    break;
                }

                System.out.println(message);
                System.out.print("서버에 보낼 메세지:");
            }
        }
    }

    public static void main(String[] args) {
        ChatClient client = new ChatClient();
        try {
            client.go();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public void closeAll() throws IOException {
        if (pw != null)
            pw.close();
        if (sc != null)
            sc.close();
        if (br != null)
            br.close();
        if (socket != null)
            socket.close();
    }

}

Client

Socket socket = new Socket("localhost", 포트번호);

Socket socket = new Socket(new InetSocketAddress("localhost", 포트번호));

Client에서 Socket을 사용하기 위한 두 가지 방법입니다. 연결하려는 외부 서버의 IP주소 대신 도메인 이름을 알고 있을 때 InetSocketAddress class를 사용할 수 있습니다.

profile
Dragon

0개의 댓글