관리 메뉴

나만을 위한 블로그

[JAVA] 소켓 프로그래밍이란? 클라이언트/서버 채팅 예제 본문

JAVA

[JAVA] 소켓 프로그래밍이란? 클라이언트/서버 채팅 예제

참깨빵위에참깨빵_ 2022. 3. 6. 10:29
728x90
반응형

안드로이드에서 채팅을 만들어보고 싶어서 찾아보다 보면 소켓 프로그래밍 또는 소켓 통신이라는 말을 자주 보게 된다.

나중에 채팅 기능을 구현할 날이 올 수도 있겠다 싶어서 미리 조금 공부하고 포스팅해두려고 한다.

 

먼저 소켓이란 무엇인가? 소켓의 사전적 정의는 아래와 같다.

콘센트 / (플러그 등을) 꽂는 곳 / (다른 부분이 들어갈 수 있도록) 푹 들어간 곳, 구멍

 

이런 의미를 가진 소켓이 개발로 넘어오면 무슨 의미를 갖게 될까? 위키백과에서 가장 근접해 보이는 소켓의 의미는 아래와 같다.

 

https://ko.wikipedia.org/wiki/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC_%EC%86%8C%EC%BC%93

 

네트워크 소켓 - 위키백과, 우리 모두의 백과사전

네트워크 소켓(network socket)은 컴퓨터 네트워크를 경유하는 프로세스 간 통신의 종착점이다. 오늘날 컴퓨터 간 통신의 대부분은 인터넷 프로토콜을 기반으로 하고 있으므로, 대부분의 네트워크

ko.wikipedia.org

네트워크 소켓은 컴퓨터 네트워크를 경유하는 프로세스 간 통신의 종착점이다. 오늘날 컴퓨터 간 통신 대부분은 인터넷 프로토콜을 기반으로 하고 있으므로, 대부분의 네트워크 소켓은 인터넷 소켓이다. 네트워크 통신을 위한 프로그램들은 소켓을 생성하고 이 소켓을 통해 서로 데이터를 교환한다. 소켓은 RFC 147에 기술사항이 정의돼 있다. 인터넷 소켓은 아래 요소들로 구성돼 있다

- 인터넷 프로토콜(TCP, UDP, raw IP)
- 로컬 IP 주소
- 로컬 포트
- 원격 IP 주소
- 원격 포트

인터넷 소켓은 크게 2개 타입으로 분류할 수 있다

- UDP 프로토콜을 사용하는 경우
- TCP 프로토콜을 사용하는 경우

 

프로그램들이 데이터를 교환할 수 있는 장소가 소켓 같다. 종착점이라고 써놨으니 어떤 장소라고 생각해도 되지 않을까 싶다.

다른 곳에선 소켓을 어떻게 설명하는지 찾아봤다.

 

https://www.ibm.com/docs/en/i/7.1?topic=communications-socket-programming 

소켓은 네트워크에서 이름을 지정하고 주소를 지정할 수 있는 통신 연결 지점(엔드포인트)이다...(중략)...소켓을 사용하는 프로세스는 동일한 시스템 또는 다른 네트워크의 다른 시스템에 있을 수 있다. 소켓을 사용하면 동일한 시스템 또는 네트워크를 통해 프로세스 간 정보를 교환하고 작업을 분산할 수 있으며 중앙집중식 데이터에 쉽게 액세스할 수 있다. 소켓 API는 TCP/IP의 네트워크 표준이다.

 

https://www.edureka.co/blog/socket-programming-in-java/

 

Socket Programming in Java | Java Networking Tutorial | Edureka

Java Socket programming is used for communication between the applications running on different JRE. Java Socket programming can be connection-oriented or connectionless.

www.edureka.co

자바의 소켓은 네트워크에서 실행되는 두 프로그램 간의 양방향 통신 링크의 한 끝점(엔드포인트)이다. 소켓은 포트 번호에 연결돼 TCP 계층에서 데이터가 전송될 애플리케이션을 식별할 수 있다. 엔드포인트는 IP 주소와 포트 번호 조합이다

 

매우 추상적인 설명이긴 한데 확실히 알 수 있는 건 소켓은 IP 주소, 포트 번호가 합쳐진 형식이고 서로 다른 프로그램이 서로 데이터 통신을 할 수 있게 도와주는 일종의 통로란 것이다.

그럼 소켓 프로그래밍은 이 통로 역할을 하는 소켓을 써서 데이터 통신을 하는 프로그래밍 방식을 말하는 게 아닌가 생각된다. 소켓 프로그래밍이 뭔지 검색해봤다.

 

https://www.section.io/engineering-education/socket-programming-in-java/

 

Understanding Socket Programming in Java

This tutorial will introduce the reader to the basics of socket programming in Java. They will understand how clients and servers communicate over the internet.

www.section.io

소켓 프로그래밍은 네트워크를 통해 두 컴퓨터 간에 데이터를 통신하는 수단이다. 연결 지향 프로토콜 또는 비연결 프로토콜을 사용해 연결할 수 있다. 데이터를 교환하기 전에 컴퓨터는 연결 지향 프로토콜을 위한 링크를 설정해야 한다. UDP는 비연결형 프로토콜의 유일한 옵션이다...(중략)

 

https://www.infoworld.com/article/2853780/socket-programming-for-scalable-systems.html

 

Socket programming in Java: A tutorial

Three iterations of a Java socket client-server example demonstrate the timeless utility of Java I/O

www.infoworld.com

소켓 프로그래밍은 서로 통신하는 두 시스템으로 요약된다. 일반적으로 네트워크 통신은 TCP(통신 제어 프로토콜)과 UDP(사용자 데이터그램 프로토콜)의 2가지 방식으로 제공된다. TCP, UDP는 다른 용도로 사용되며 둘 다 제약 조건이 있다.

- TCP는 클라이언트가 서버에 연결하고 두 시스템이 통신할 수 있게 하는 비교적 간단하고 안정적인 프로토콜이다. TCP에서 각 엔티티는 통신 페이로드가 수신됐음을 알고 있다
- UDP는 연결이 없는 프로토콜이며 미디어 스트리밍과 같이 모든 패킷이 대상에 도착할 필요가 없는 시나리오에 적합하다

소켓은 TCP를 사용하는 두 컴퓨터 간의 통신 메커니즘을 제공한다. 클라이언트 프로그램은 통신 끝에 소켓을 만들고 해당 소켓을 서버에 연결하려고 시도한다. 연결이 생성되면 서버는 통신이 끝날 때 소켓 객체를 만든다. 클라이언트와 서버는 이제 소켓에 쓰고 읽는 방식으로 통신할 수 있다. java.net.Socket 클래스는 소켓을 나타내며 java.net.ServerSocket 클래스는 서버 프로그램이 클라이언트를 수신 대기하고 클라이언트와의 연결을 설정하는 메커니즘을 제공한다. 소켓을 써서 두 컴퓨터 간에 TCP 연결 설정 시 다음 단계가 발생한다.

1. 서버는 어떤 포트 번호 통신이 발생하는지 나타내는 ServerSocket 객체를 인스턴스화한다
2. 서버는 ServerSocket 클래스의 accept()를 호출한다. 이 메서드는 클라이언트가 지정된 포트의 서버에 연결할 때까지 기다린다
3. 서버가 대기한 후 클라이언트는 연결할 서버 이름과 포트 번호를 지정해서 Socket 객체를 인스턴스화한다
4. Socket 클래스의 생성자는 클라이언트를 지정된 서버 및 포트 번호에 연결하려고 시도한다. 통신이 설정되면 클라이언트는 서버와 통신할 수 있는 Socket 객체를 갖게 된다
5. 서버 측에서 accept()는 클라이언트 소켓에 연결된 서버의 새 소켓에 대한 참조를 반환한다

연결이 설정되면 I/O 스트림을 사용해 통신할 수 있다. 각 소켓에는 OutputStream과 InputStream이 모두 있다. 클라이언트의 OutputStream은 서버의 InputStream에 연결되고, 클라이언트의 InputStream은 서버의 OutputStream에 연결된다. TCP는 양방향 통신 프로토콜이므로 두 스트림을 통해 동시에 데이터를 보낼 수 있다...(중략)

 

이제 코드로 확인해보자. 아래는 구글링하다 보면 찾을 수 있는 클라이언트/서버 구조의 채팅 예제다.

 

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class Server {
    public static void main(String[] args) {
        ServerSocket serverSocket;
        Socket socket;

        try {
            serverSocket = new ServerSocket(7777);
            System.out.println("!!! 서버가 준비됐습니다");
            socket = serverSocket.accept();
            Sender sender = new Sender(socket);
            Receiver receiver = new Receiver(socket);

            sender.start();
            receiver.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class Sender extends Thread {
    Socket socket;
    DataOutputStream out;
    String name;

    Sender(Socket socket) {
        this.socket = socket;
        try {
            out = new DataOutputStream(socket.getOutputStream());
            name = "[" + socket.getInetAddress() + ":" + socket.getPort() + "]";
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        Scanner sc = new Scanner(System.in);
        while (out != null) {
            try {
                out.writeUTF(name + sc.nextLine());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

class Receiver extends Thread {
    Socket socket;
    DataInputStream in;

    Receiver(Socket socket) {
        this.socket = socket;
        try {
            in = new DataInputStream(socket.getInputStream());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while (in != null) {
            try {
                System.out.println(in.readUTF());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;

public class Client {
    public static void main(String[] args) {
        try {
            String serverIp = "127.0.0.1";
            Socket socket = new Socket(serverIp, 7777);
            System.out.println("서버에 연결됨");
            Sender sender = new Sender(socket);
            Receiver receiver = new Receiver(socket);

            sender.start();
            receiver.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

클라이언트, 서버 코드를 모두 복붙했다면 서버 코드부터 실행시킨다. 아주 아주 만약에 왜 서버부터 실행시키는지 궁금한 사람은 클라이언트 파일부터 먼저 실행해보면 왜 그렇게 말했는지 알게 될 것이다. 롤 같은 온라인 게임이 서버를 끈 상태로 서비스를 오픈하지는 않을 것이다.

서버 파일을 시작하면 콘솔에 아래와 같은 문구가 뜬다.

 

 

이제 이 상태에서 클라이언트 파일도 실행해보자. 인텔리제이 기준으로 서버 실행 후 클라이언트 파일 실행 시 콘솔 창이 하나 더 생기며 서버, 클라이언트가 구분된다.

 

 

이제 이 상태에서 클라이언트 콘솔창에 메시지를 입력해본다. 스캐너를 사용했기 때문에 콘솔창에 입력한 후 엔터를 치면 내가 입력한 내용이 서버로 전송된다.

 

 

클라이언트에서 입력한 내용이 서버로 전달돼 콘솔에 출력되는 걸 볼 수 있다. 서버 쪽에서도 콘솔창에 뭔가를 입력해보자.

 

 

클라이언트도 서버가 보낸 메시지를 받아서 콘솔에 출력하는 걸 볼 수 있다. 이걸 응용한다면 안드로이드 앱에서 TCP/IP를 활용한 채팅 기능 구현도 할 수 있다.

코드를 봤으면 이제 코드 분석도 해보자. 먼저 서버 측에 사용된 ServerSocket과 Sender, Receiver란 이름의 쓰레드 2개가 눈에 띈다.

이름에서 알 수 있듯 Sender는 메시지를 보내는 역할을 하고 Receiver는 메시지를 받는 역할을 하는 쓰레드다.

그리고 각 쓰레드는 Socket 객체를 하나씩 갖고 있고 Sender는 DataOutputStream, Receiver는 DataInputStream을 각각 갖고 있는 게 보인다.

이 클래스들을 각각 확인해 보면 서버 코드를 좀 더 잘 알 수 있게 되지 않을까? 오라클 공식문서에서 설명하는 DataOutputStream, DataInputStream은 아래와 같다.

 

https://docs.oracle.com/javase/7/docs/api/java/io/DataOutputStream.html

 

DataOutputStream (Java Platform SE 7 )

Writes a string to the underlying output stream using modified UTF-8 encoding in a machine-independent manner. First, two bytes are written to the output stream as if by the writeShort method giving the number of bytes to follow. This value is the number o

docs.oracle.com

DataOutputStream을 쓰면 애플리케이션이 기본 자바 자료형을 outputStream에 이식 가능한 방식으로 작성할 수 있다. 그 다음 애플리케이션은 DataInputStream을 써서 데이터를 다시 읽을 수 있다

 

https://docs.oracle.com/javase/7/docs/api/java/io/DataInputStream.html

 

DataInputStream (Java Platform SE 7 )

Reads some number of bytes from the contained input stream and stores them into the buffer array b. The number of bytes actually read is returned as an integer. This method blocks until input data is available, end of file is detected, or an exception is t

docs.oracle.com

DataInputStream을 쓰면 애플리케이션이 기본 입력 스트림에서 기계 독립적인 방식으로 원시 자바 데이터 유형을 읽을 수 있다. 애플리케이션은 DataOutputStream을 써서 나중에 DataInputStream에서 읽을 수 있는 데이터를 쓴다(write). DataInputStream은 다중 쓰레드 접근에 반드시 안전한 것은 아니다. 쓰레드 세이프는 선택 사항이고 이 클래스 메서드 사용자의 책임이다

 

DataInputStream은 데이터를 쓸 때 사용하는 클래스고 DataOutputStream은 데이터를 읽을 때 사용하는 클래스란 것 같다.

공식문서만으론 이해하기 힘드니 다른 사람들이 쓴 포스팅을 보자.

 

 

https://xzio.tistory.com/311

 

[JAVA] ByteStream : DataInputStream / DataOutputStream

ByteStream : DataInputStream / DataOutputStream DataInputStream과 DataOutputStream은 자바의 기본 자료형 데이터를 바이트 스트림으로 입출력하는 기능을 제공하는 ByteStream 클래스이다. DataInputStream..

xzio.tistory.com

DataInputStream, DataOutputStream은 자바의 기본 자료형 데이터를 바이트 스트림으로 입출력하는 기능을 제공하는 ByteStream 클래스다. 이들은 FileInput/OutputStream을 상속하고 있어 객체 생성 시 Input/OutputStream을 인자로 가진다. 이 클래스와 입출력 장치를 대상으로 하는 입출력 클래스를 같이 쓰면 자바의 기본 자료형 데이터를 파일 등 입출력 장치로 직접 입출력할 수 있다.
FileInput/OutputStream의 차이점은 자바 기본형 데이터를 입출력할 수 있다는 것이다. FileInput/OutputStream은 바이트 배열 단위의 데이터만 입출력할 수 있었다. 하지만 DataStream Filter를 적용해서 자바 기본형(char, int, long 등)으로 데이터를 입출력할 수 있다

 

http://tutorials.jenkov.com/java-io/datainputstream.html

 

Java DataInputStream

The Java DataInputStream class in Java IO enables you to read primitive types (int, float etc) from an underlying InputStream. This Java DataInputStream tutorial explains how to use the DataInputStream in Java.

tutorials.jenkov.com

자바 DataInputStream 클래스인 java.io.DataInputStream을 쓰면 원시 바이트 대신 InputStream에서 자바 기본형(int, float, long 등)을 읽을 수 있다. DataInputStream에서 InputStream을 래핑한 다음 DataInputStream을 통해 자바 기본형을 읽을 수 있다. 이것이 DataInputStream이라 불리는 이유다. 바이트가 아닌 데이터(숫자)를 읽기 때문이다
DataInputStream은 읽어야 하는 데이터가 int, long, float, double 같이 1바이트보다 큰 자바 기본형으로 구성된 경우 편리하다...(중략)

 

정리하면 DataInput/OutputStream은 각각 FileInput/OutputStream이란 클래스를 상속하고 있어서 객체 생성 시 Input/OutputStream을 가져야 한다는 걸 알았다. 나머지 설명은 공식문서 설명과 딱히 다른 게 없다.

그리고 DataInput/OutputStream이 가져야 하는 인자인 Input/OutputStream은 예제 코드 상 Sender, Receiver의 생성자로 각각 받은 Socket 객체에서 얻어 사용하는 걸 볼 수 있다.

추가로 Sender의 생성자를 보면 try-catch문 안에서 인스턴스 변수로 선언한 String name에 Socket 객체를 통해 얻은 InetAddress와 포트 번호를 ":" 기호와 합쳐서 누가 보냈는지 표시하도록 처리한 걸 볼 수 있다.

 

그리고 쓰레드가 수행할 작업을 정의하는 run()에서 writeUTF()와 readUTF()가 사용된 걸 볼 수 있다.

Ctrl+좌클릭으로 메서드의 자바독 주석을 확인해보면 인텔리제이 기준 아래와 같이 나온다.

 

writeUTF(String str) : 시스템 독립적인 방식으로 수정된 UTF-8 인코딩을 써서 기본 출력 스트림에 문자열을 쓴다. 먼저 writeShort 메서드가 따라야 할 바이트 수를 제공하는 것처럼 2바이트가 출력 스트림에 기록된다. 이 값은 문자열의 길이가 아니라 실제 기록된 바이트 수다. 길이에 따라 문자열의 각 문자는 해당 문자에 대해 수정된 UTF-8 인코딩을 써서 순서대로 출력된다. 예외가 발생하지 않으면 기록된 카운터는 출력 스트림에 기록된 총 바이트 수만큼 증가한다. 이것은 적어도 str의 길이에 2를 더한 값이 될 것이고 최대 2에 str의 길이를 더한 값의 3배가 될 것이다.

readUTF(DataInput in) : 수정된 UTF-8 형식으로 인코딩된 유니코드 문자열 표현으로 스트림에서 읽는다. 이 문자열은 String으로 반환된다. 수정된 UTF-8 표현의 세부 사항은 DataInput의 readUTF()와 동일하다

 

뭔가 길게 써 있는데 요약하면 그냥 UTF-8 방식으로 데이터를 쓰거나 읽는 메서드다.

이 메서드 말고도 readByte()나 readShort() 등 메서드가 있는데, 이 메서드들은 n바이트를 읽어서 해당 메서드명을 구성하는 자료형의 값을 반환한다. readByte()면 1바이트를 읽어서 Byte 값을 반환하거나 readLong()은 8바이트를 읽어 long 값을 반환하거나 한다.

 

이걸 바탕으로 공부하면 안드로이드 앱으로 채팅 어플을 만들 수도 있을 것이다.

단 안드로이드는 UI까지 신경써야 하니 위의 예제 코드에 추가되는 부분이 있을 것이고 이를 고려해야 할 것이다.

반응형
Comments