콘텐츠로 이동

Net과 소켓 통신

unibase.net에는 연결하는 쪽의 Socket, 연결을 수락하는 쪽의 ServerSocket, 주소를 담는 SocketAddress가 선언되어 있습니다. 이 장에서는 각 객체의 역할과 호출 순서를 살펴봅니다. 아래 두 소스는 수신 측과 송신 측을 함께 실행합니다. 빌드·실행을 누르면 실제 TCP로 받은 문자열과 응답을 확인할 수 있습니다. 각 실행은 자체 네트워크 공간을 사용하므로 별도 서버 준비 없이 두 측의 동작을 살펴볼 수 있습니다.

SocketAddress는 host와 port를 보관합니다. Socket()으로 만든 객체에 connect(address)를 호출하거나 Socket(host, port) 생성자 형태를 사용할 수 있습니다. 아래 소스는 연결 단계가 보이도록 첫 번째 형태를 사용합니다.

ServerSocket(0)으로 수신 소켓을 먼저 만들고 port()로 배정된 포트를 얻습니다. Receiver는 별도 Thread에서 접속을 수락합니다. 주 실행 흐름은 해당 포트에 연결하고, output()에서 얻은 PrintStream으로 문자열을 보냅니다.

import unibase.io.PrintStream;
import unibase.io.InputStream;
import unibase.net.Socket;
import unibase.net.SocketAddress;
import unibase.net.ServerSocket;
import unibase.concurrent.Runnable;
import unibase.concurrent.Thread;

class Receiver implements Runnable {
    private ServerSocket server;
    public string received = "";

    public Receiver(ServerSocket server) {
        this.server = server;
    }

    public void run() {
        Socket client = this.server.accept();
        InputStream input = client.input();
        this.received = input.readLine();
        PrintStream output = client.output();
        output.println("ack");
        client.close();
    }
}

class SocketSend {
    static void main() {
        ServerSocket server = ServerSocket(0);
        Receiver receiver = Receiver(server);
        Thread worker = Thread("receiver", receiver);
        worker.start();
        Socket socket = Socket();
        socket.connect(SocketAddress("127.0.0.1", server.port()));
        PrintStream output = socket.output();
        output.println("ping 🌐");
        InputStream input = socket.input();
        string reply = input.readLine();
        socket.close();
        worker.join();
        server.close();
        print(receiver.received);
        print(reply);
    }
}

다른 언어의 탭은 직접 작성한 대응 코드이며 컴파일러의 자동 생성 결과가 아닙니다. 언어마다 API와 실행 환경이 다릅니다. 이 페이지의 빌드·실행은 Unibase 탭에서 사용할 수 있습니다.

서버 실행을 준비 중입니다.
출력 비교 기준
ping 🌐
ack
Unibase 소스 내려받기

검증 상태: 문법 검사 통과 · 실행 출력 일치 (2026-10-06)

읽기 기준 결과와 실제 실행 출력 비교

코드를 읽어 정한 비교 기준입니다. 실행 성공 결과로 대신 표시하지 않습니다.

ping 🌐
ack

실제 네이티브 실행: 빌드 passed, 실행 passed, 종료 코드 0

ping 🌐
ack
현재 실행 확인 범위

호출 순서는 수신 준비 → Thread 시작 → 주소 준비·연결 → 전송 → 응답 읽기 → 연결 종료 → Thread 완료 대기입니다. 출력의 ping 🌐는 Receiver.input.readLine()으로 실제 읽어 보관한 값이고, ack는 송신 측이 읽은 응답입니다. 보내려던 문자열을 콘솔에 다시 출력하는 방식으로 통신 성공을 판단하지 않습니다.

isConnected(), isClosed(), remoteHost(), remotePort()도 선언되어 있습니다. 연결 상태만으로 상대가 메시지를 처리했다고 판단하지 마세요.

ServerSocket(port)는 수신 측을 표현하고, accept()는 연결된 상대를 나타내는 Socket을 반환하는 선언입니다. 반환된 객체의 출력 스트림으로 메시지를 보낼 수 있습니다.

import unibase.io.PrintStream;
import unibase.io.InputStream;
import unibase.net.ServerSocket;
import unibase.net.Socket;
import unibase.concurrent.Runnable;
import unibase.concurrent.Thread;

class Sender implements Runnable {
    private int port;
    public string reply = "";

    public Sender(int port) {
        this.port = port;
    }

    public void run() {
        Socket socket = Socket("127.0.0.1", this.port);
        PrintStream output = socket.output();
        output.println("hello");
        InputStream input = socket.input();
        this.reply = input.readLine();
        socket.close();
    }
}

class ServerSocketExample {
    static void main() {
        ServerSocket server = ServerSocket(0);
        Sender sender = Sender(server.port());
        Thread worker = Thread("sender", sender);
        worker.start();
        Socket client = server.accept();
        InputStream input = client.input();
        string received = input.readLine();
        string peer = client.remoteHost();
        PrintStream output = client.output();
        output.println("received");
        client.close();
        worker.join();
        server.close();
        print(peer);
        print(received);
        print(sender.reply);
    }
}

다른 언어의 탭은 직접 작성한 대응 코드이며 컴파일러의 자동 생성 결과가 아닙니다. 언어마다 API와 실행 환경이 다릅니다. 이 페이지의 빌드·실행은 Unibase 탭에서 사용할 수 있습니다.

서버 실행을 준비 중입니다.
출력 비교 기준
127.0.0.1
hello
received
Unibase 소스 내려받기

검증 상태: 문법 검사 통과 · 실행 출력 일치 (2026-10-06)

읽기 기준 결과와 실제 실행 출력 비교

코드를 읽어 정한 비교 기준입니다. 실행 성공 결과로 대신 표시하지 않습니다.

127.0.0.1
hello
received

실제 네이티브 실행: 빌드 passed, 실행 passed, 종료 코드 0

127.0.0.1
hello
received
현재 실행 확인 범위

이 소스에서는 주 흐름이 수신 소켓을 담당하고 Sender Thread가 접속합니다. accept()로 얻은 연결에서 hello를 읽고 received를 응답합니다. 출력은 상대 주소 127.0.0.1, 서버가 읽은 hello, 송신 작업이 읽은 received 순서입니다. Thread 완료 뒤 결과를 읽으므로 응답 저장이 끝난 시점이 명확합니다. 반복 수락, 여러 클라이언트 처리, 시간 제한, 예외 발생 시 자원 정리는 추가 설계와 런타임 검증이 필요합니다. 위 코드는 정상 흐름의 종료 위치를 설명합니다.

PrintStream.print(string)는 문자열 bytes를 보내고 println(string)는 끝에 LF를 추가합니다. Socket.input()에서 얻은 InputStream.readLine()은 LF까지 읽어 줄바꿈을 제외한 문자열을 반환합니다. CRLF인 경우 마지막 CR도 제외합니다. UTF-8 소스의 한글·이모지도 같은 bytes로 전달합니다.

readLine()은 데이터가 올 때까지 대기합니다. 줄의 최대 길이는 현재 native 구현에서 1 MiB입니다. 데이터를 하나도 읽기 전에 연결이 끝나거나 읽기 오류가 발생하면 런타임 오류로 종료합니다. 줄의 일부를 읽은 뒤 EOF를 만나면 그 부분을 반환합니다. 서로 응답을 기다리기만 하는 코드는 완료되지 않으므로 누가 먼저 보내는지를 정하세요.

현재 native 예제 환경의 Socket은 숫자 IPv4 주소를 사용하며, ServerSocket은 loopback에 bind합니다. 포트 0은 사용 가능한 포트를 자동 배정받는 값입니다. 문서 실행 서버는 같은 실행 안의 TCP만 허용합니다. DNS·외부 서버·HTTP 라우팅·TLS·gRPC와 다른 타겟의 지원은 현재 실행 확인 범위에서 구분합니다.

선언 형태 역할
Socket() 연결 단계를 별도로 수행하는 객체 생성
Socket(string host, int port) 주소를 받는 생성자 형태
void connect(SocketAddress) 주소 객체로 연결 요청
void connect(string host, int port) host·port로 연결 요청
InputStream input() 입력 스트림 조회
PrintStream output() 출력 스트림 조회
void close() 종료 요청
bool isConnected(), bool isClosed() 연결·종료 상태 조회
string remoteHost(), int remotePort() 원격 주소 조회

첫 소스는 기본 생성자 뒤 connect()를 호출하고, 두 번째는 Socket(host, port) 생성자로 바로 연결합니다. 같은 연결에 두 방식을 중복 호출할 필요는 없습니다.

선언 형태 역할
ServerSocket(int port) 수신 포트를 받는 생성자
Socket accept() 상대 연결 객체 반환
void close() 수신 소켓 종료
bool isOpen() 열림 상태 조회
int port() 포트 조회

현재 선언에는 수신 주소 지정이나 accept timeout 매개변수가 없습니다. 위 예제는 IPv4 loopback과 자동 배정 포트를 사용합니다. 프로세스의 시간 제한이 소켓별 timeout API를 뜻하지는 않습니다.

수신 소켓과 연결된 클라이언트 소켓은 역할이 다릅니다. 서버를 닫는 것과 특정 클라이언트를 닫는 것을 구분하세요. 전송에 사용한 연결은 정상 흐름에서 종료하며, 예외 발생 시에도 종료하는 흐름은 별도 설계가 필요합니다.

실패 위치 관찰할 대상
연결 전 host·port·상대의 준비 상태
연결 요청 실제 연결 실패 진단
전송 중 상대의 수신 여부·연결 상태
수락 대기 클라이언트 접속 여부·대기 제한
종료 자원 종료와 남은 작업의 관계

TCP를 사용할 때는 연결과 메시지 처리 완료를 구분해야 합니다. 한 번의 println() 호출이 상대의 한 번의 읽기와 정확히 대응한다고 이 선언만으로 확정할 수 없습니다. 수신 메시지를 구분하는 프로토콜·인코딩·길이 제한은 별도 계약으로 다뤄야 합니다.

송신 코드의 검증에는 메시지를 읽는 수신 측이, 수신 코드의 검증에는 접속해 응답을 읽는 클라이언트가 필요합니다. 위 소스는 두 측을 하나의 실행에서 구성하고 실제 수신 결과를 비교합니다. 새 실행은 독립된 공간을 사용하므로 다른 사용자의 listener와 포트가 충돌하지 않습니다.

Thread로 접속 처리를 분리할 때도 연결마다 담당 작업과 종료 책임을 정해야 합니다. Thread의 완료 대기와 소켓의 종료가 서로 다른 동작이라는 점은 Thread와 비동기 작업을 함께 읽으면 확인할 수 있습니다.