개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

기초개념 › 네트워크

IP 주소와 포트

IP 주소와 포트는 각각 무엇을 가리키는가

네트워크로 다른 프로그램에 연결하려면 두 가지를 알아야 합니다. IP 주소는 "어느 컴퓨터인지"를, 포트는 "그 컴퓨터에서 돌고 있는 여러 프로그램 가운데 어느 프로그램인지"를 가리키는 번호입니다. 코드 칸의 서버 프로그램은 new ServerSocket(8080)으로 운영체제에 8080번 포트를 달라고 해서 열고 연결을 기다리며, 클라이언트 프로그램은 new Socket("127.0.0.1", 8080)으로 127.0.0.1 컴퓨터(지금 이 컴퓨터 자신)의 8080번 포트로 연결합니다. 이 실습은 서버 프로그램을 띄우지 않거나, 아무도 열지 않은 9090번 포트로 연결하거나, 이미 열려 있는 8080번 포트를 다른 프로그램이 또 열게 하면서 연결이 어떻게 되는지를 봅니다. 서버와 클라이언트는 서로 다른 프로그램이라서, 코드 칸에는 두 프로그램의 코드가 함께 적혀 있습니다.

실행으로 대조 · JDK 21(Windows 11)에서 서버, 또 다른 프로그램, 클라이언트를 별도 프로세스로 띄워 여덟 가지 조합을 127.0.0.1에서 실행해 대조

이 실습에 나오는 개념

IP 주소(IPv4)
인터넷에서 기기를 찾아가는 데 쓰는 32비트 숫자 주소다. 사람이 읽기 쉽게 8비트씩 끊어 192.168.0.10처럼 0부터 255까지의 10진수 네 개로 적는다. 주소의 앞부분은 어느 네트워크인지를, 뒷부분은 그 네트워크 안의 어느 기기인지를 나타낸다. 어디까지가 앞부분인지는 주소만 보아서는 알 수 없고 서브넷 마스크가 함께 있어야 정해진다.
포트
한 컴퓨터에서 돌고 있는 여러 프로그램 가운데 누가 데이터를 받을지 구분하는 0부터 65535까지의 번호다. IP 주소가 컴퓨터를 가리킨다면 포트는 그 컴퓨터 안의 프로그램을 가리키므로, 접속할 곳은 "IP 주소:포트"로 적는다. 서버 프로그램은 정해 둔 포트에서 연결을 기다리며, 웹 서버의 80번(HTTP)과 443번(HTTPS)처럼 널리 약속된 번호도 있다. 한 포트에서 연결을 기다릴 수 있는 프로그램은 보통 하나뿐이어서, 이미 쓰이는 포트에 서버를 또 띄우면 "주소가 이미 사용 중"이라는 오류가 난다. TCP의 포트와 UDP의 포트는 따로 관리되므로 번호가 같아도 서로 다른 것이다.
소켓
프로그램이 네트워크로 데이터를 주고받을 때 쓰는 통신의 끝점이며, 운영체제가 만들어 프로그램에 건네준다. 프로그램은 소켓에 쓰고 소켓에서 읽기만 하고, 실제로 데이터를 나누어 보내고 받는 일은 운영체제가 맡는다. TCP 연결 하나는 양쪽 끝의 소켓 한 쌍으로 이루어지고, 운영체제는 보내는 IP 주소, 보내는 포트, 받는 IP 주소, 받는 포트의 네 값으로 연결을 구분한다. 그래서 서버는 포트 하나로 여러 클라이언트의 연결을 함께 받을 수 있고, 연결마다 소켓이 하나씩 따로 생긴다. 다 쓴 소켓을 닫지 않으면 운영체제의 자원이 계속 묶인다.
127.0.0.1(localhost)
127.0.0.1은 "이 코드가 돌고 있는 컴퓨터 자신"을 가리키는 IP 주소다. 127로 시작하는 주소는 모두 호스트 내부의 루프백(loopback) 주소로, RFC 1122는 이런 주소가 호스트 밖에 나타나서는 안 된다고 정한다(3.2.1.3절). 그래서 이 주소로 보낸 데이터는 컴퓨터 밖으로 나가지 않고 같은 컴퓨터 안의 프로그램에 닿는다. localhost는 이 주소를 가리키는 이름이며, RFC 6761은 이름을 주소로 바꾸는 소프트웨어가 localhost를 루프백 주소로 바꾸도록 권고한다(6.3절). 서버와 클라이언트를 한 컴퓨터에서 함께 돌려 볼 때 쓰며, 내 컴퓨터의 127.0.0.1은 다른 컴퓨터에서 접속할 수 없다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 서버 프로그램이 시작해 new ServerSocket(8080) 줄을 실행합니다. 이 줄은 운영체제에 "8080번 포트로 오는 연결은 나에게 달라"고 요청하는 것입니다. 운영체제가 요청을 받아 주어, 이제 이 컴퓨터에서 8080번 포트를 연 프로그램은 서버 프로그램입니다.
  2. server.accept() 줄을 실행합니다. accept는 연결이 올 때까지 기다리는 메서드여서, 서버 프로그램은 이 줄에서 멈춘 채 연결을 기다립니다.
  3. 클라이언트 프로그램이 new Socket("127.0.0.1", 8080) 줄을 실행합니다. 127.0.0.1은 이 코드가 돌고 있는 컴퓨터 자신을 가리키는 주소이고(localhost라는 이름이 이 주소를 뜻합니다), 8080은 그 컴퓨터에서 연결하려는 프로그램의 포트 번호입니다. IP 주소는 어느 컴퓨터인지를, 포트는 그 컴퓨터의 어느 프로그램인지를 가리키므로, 클라이언트는 이 컴퓨터의 8080번 포트로 연결해 달라고 요청합니다.
  4. 운영체제가 8080번 포트를 연 프로그램을 찾습니다. 서버 프로그램이 열어 두었으므로 연결을 서버 프로그램에 넘깁니다. 서버의 server.accept() 줄이 끝나고, 이 연결로 글을 주고받는 소켓을 fromClient에 담아 돌려줍니다.
  5. 클라이언트 쪽 소켓에도 자기 포트 번호가 있습니다. 코드에서 정한 적이 없는데도 서버의 포트와 다른 번호가 붙어 있었습니다(대조 실행에서 본 번호는 모두 49152 이상). 연결 하나는 양쪽 끝의 IP 주소와 포트의 쌍으로 구분됩니다.
  6. 서버 프로그램이 fromClient에 "안녕하세요"를 쓰고(write), fromClient.close()로 이 연결을 닫습니다.
  7. 클라이언트의 readAllBytes는 연결이 닫힐 때까지 온 글을 모두 읽습니다. 클라이언트가 "안녕하세요"를 text에 담고 println이 출력합니다.

실행 결과

코드

// 서버 프로그램: 8080번 포트를 열고 연결을 기다린다
ServerSocket server = new ServerSocket(8080);
Socket fromClient = server.accept();
fromClient.getOutputStream().write("안녕하세요".getBytes("UTF-8"));
fromClient.close();

// 클라이언트 프로그램: 127.0.0.1(이 컴퓨터)에 연결한다
Socket socket = new Socket("127.0.0.1", 8080);
String text = new String(socket.getInputStream().readAllBytes(), "UTF-8");
System.out.println(text);

이 모형의 전제

자바는 JDK 21.0.12.1이고 운영체제는 Windows 11입니다. 여덟 가지 조합을 모두, 화면의 코드와 같은 글로 서버 프로그램, 또 다른 프로그램, 클라이언트 프로그램의 소스를 만들어 컴파일하고, 이 JDK의 java를 서로 다른 프로세스로 띄워 127.0.0.1에서 실제로 연결한 결과와 대조했습니다. 대조한 것은 클라이언트의 연결이 되었는지와 던져진 예외의 종류, 클라이언트가 받은 글, 또 열려던 프로그램이 열었는지와 던져진 예외의 종류입니다. 화면의 8080과 9090은 예시 번호이고, 실제 대조에서는 이 PC에서 비어 있는 다른 번호 두 개를 조합마다 골라 8080 자리와 9090 자리에 썼습니다(9090 자리에는 아무도 열지 않은 것을 연결해 보아 확인했습니다). 화면의 코드는 import 줄, main 메서드, 예외 처리(throws Exception으로 넘김)를 줄였고, 실제 프로그램에는 프로세스가 준비되었음을 알리는 출력 줄이 더 있습니다. 서버와 클라이언트를 실제로 연결한 것은 127.0.0.1뿐이고 외부 네트워크에는 연결하지 않았습니다. 다만 new ServerSocket(포트)는 이 컴퓨터의 모든 주소에서 연결을 받는 모양으로 열려서(netstat에도 0.0.0.0으로 나왔습니다), 대조하는 동안 서버 프로그램은 몇 초 동안 127.0.0.1에만 묶이지 않은 상태였습니다. 연결 거절과 "이미 쓰는 포트" 오류는 운영체제(Windows 11)가 돌려준 것을 자바가 예외로 알린 것입니다. 연결을 거절당했을 때 자바가 던진 것은 java.net.ConnectException(메시지 Connection refused: connect), 이미 열린 포트를 또 열 때 던진 것은 java.net.BindException(메시지 Address already in use: bind)이었고, 이 메시지는 이 PC에서 관찰한 것이어서 운영체제마다 다를 수 있습니다. 실행으로 확인한 것은 서버가 포트를 연 직후 운영체제의 목록(netstat)에 그 포트가 서버 프로세스의 것으로 올라 있다는 것, 클라이언트 쪽 소켓의 자기 포트가 서버 포트와 다른 49152 이상의 번호라는 것, 서버 없이 또 다른 프로그램만 포트를 열면 클라이언트의 연결은 되지만 3초 안에 글이 오지 않고 클라이언트가 끝나지 않았다는 것(3초 뒤 일은 보지 않았습니다), 서버가 열지 않은 포트로 연결해도 서버 프로세스는 계속 기다린다는 것입니다. 실제로 운영체제가 TCP 연결 요청에 어떤 신호로 거절했는지는 패킷을 잡아 보지 않아 확인하지 못했습니다. 문서로는 다음을 직접 열어 확인했습니다. RFC 9293(TCP)의 2.2절은 TCP가 포트 번호로 응용 서비스를 구분한다고, 3.1절은 헤더에 출발지 포트와 목적지 포트가 있다고, 3.4.1절은 연결이 소켓(양쪽 끝)의 쌍으로 정해진다고, 3.10.7.1절은 연결이 없는 상태(CLOSED)에서 RST를 담지 않은 세그먼트가 오면 RST로 응답한다고 적습니다(위 거절이 이것인지는 확인하지 못했습니다). RFC 1122의 3.2.1.3절 (g)는 127로 시작하는 주소를 호스트 내부 루프백 주소로 정하고 호스트 밖에 나타나서는 안 된다고 하며, RFC 6890의 2.2.2절 Table 4는 127.0.0.0/8을 Loopback으로 올리고 전달되지 않는 주소로 표시합니다. RFC 6761의 6.3절은 localhost 이름이 루프백 주소로 풀리게 하도록 권합니다(이 PC에서 localhost를 풀면 127.0.0.1과 IPv6의 루프백 주소 ::1이 나왔고, 이 실습은 127.0.0.1만 다뤘습니다). java.net.ServerSocket 문서(Java SE 21)는 포트 번호로 서버 소켓을 만들고, 아직 받지 않은 연결 요청의 대기열 길이가 50이며, accept는 연결이 올 때까지 막힌다고 적습니다. 이 문서에는 BindException이나 이미 쓰는 포트에 대한 설명이 없고, java.net.Socket 문서의 생성자 설명에도 ConnectException이 나오지 않아 두 예외는 이 PC에서의 관찰입니다. 포트는 TCP로 연결할 때의 것이고, UDP나 IPv6, 한 컴퓨터가 여러 IP 주소를 가지는 경우, 인터넷의 다른 컴퓨터에 연결하는 경우는 다루지 않았습니다. 소켓과 포트의 세부 동작(임시 포트, 포트 번호의 범위와 약속된 번호 등)은 "세부 동작 실습: 소켓과 포트"에서 다룹니다.

앞 실습: 저장과 지속성 · 다음 실습: TCP 연결

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요