본문 바로가기

JAVA/네트워크 프로그래밍

[JAVA] 소켓 생성과 연결(Client)

소켓 생성과 연결


java.net.Socket 클래스는 클라이언트 측의 TCP 기능을 수행하기 위한 자바의 기본 클래스이다.


그리고 URL, URLConnection, Applet, JEditorPane 같은 TCP 네트워크 연결을 생성하는 클라이언트 기반의 클래스들 역시 내부적으로 결국에는 java.net.Socket의 메소드를 호출한다.


java.net.Socket 클래스 자체는 호스트 운영체제의 로컬 TCP 스택과 통신을 위해 네이티브 코드를 사용한다.



기본 생성자


각각의 소켓 생성자는 연결할 호스트와 포트를 매개변수로 전달받는다. 


호스트 매개변수는 InetAddress 또는 String 타입으로 전달되며, 포트는 1에서 65535까지 int 타입으로 전달된다.


public Socket(String host, int port) throws UnknownHostException, IOException

public Socket(InetAddress host, int port) throws IOException


이 생성자들은 소켓을 연결한다(즉, 생성자가 반환되기 전에 원격 호스트에 대한 실제 연결이 생성된다).


생성자가 다양한 이유로 소켓을 연결할 수 없는 경우, 생성자는 IOException 또는 UnknownHostException 예외를 발생시킨다.


예를 들어,


try{

Socket toOReilly = new Socket("www.oreilly.com", 80);

//데이터를 보내고 받기.....

}catch (UnknownHostException ex) {

System.err.println(ex);

}catch (IOException ex) {

System.err.println(ex);

}


생성자에서 호스트 매개변수는 단지 String으로 표현된 호스트네임이다. 도메인 네임 서버(DNS)가 동작하지 않거나 호스트네임을 주소로 변환할 수 없는 경우, 생성자는 UnknownHostException 예외를 발생시킨다.


그 외에 다양한 이유로 소켓을 열 수 없는 경우, 생성자는 IOException 예외를 발생시킨다.


연결 시도가 실패하는 데는 다양한 원인이 있다. 연결을 시도하는 대상 호스트의 포트가 연결을 허용하지 않는 경우, 호텔 와이파이 서비스가 호텔 웹 사이트에 로그인하고 돈을 결제할 때 까지 차단한 경우, 또는 라우터의 경로 제어 문제로 패킷을 목적지로 보낼 수 없는 경우도 있다.


이러한 경우 생성자는 Socket 객체를 만들 수는 없지만 원격 호스트에 대한 연결은 시도해 볼 수 있기 때문에 아래 예제와 같이 원격 호스트의 특정 포트에 대해 연결이 가능한지 확인하는 데 사용할 수 있다.


package network;


import java.io.IOException;

import java.net.Socket;

import java.net.UnknownHostException;


public class T07LowPortScanner {

public static void main(String[] args) {

String host = args.length > 0? args[0]: "localhost";

for(int i = 1; i < 1024; i++){

try{

Socket s = new Socket(host, i);

System.out.println("포트에서 실행 중인 서버" + i + "번 포트" + host);

s.close();

}catch (UnknownHostException ex){

System.err.println(ex);

break;

}catch (IOException ex){

//해당 포트에서 실행중인 서버가 없음

}

}

}

}



실행 결과 (실행결과는 컴퓨터의 환경에 따라 다르게 나온다)



단순한 프로그램이지만 시스템이 하고 있는 일을 이해하는 데에 도움을 줄 수 있으며, 이는 시스템 보안을 위한 첫걸음이다. 이 프로그램을 이용해 외부 침입자의 통로를 발견하고 차단할 수 있으며, 관리자의 통제를 벗어난 서버를 발견하는 일도 가능하다.


다음 세 개의 생성자는 연결되지 않은 소켓을 생성한다. 이 생성자들은 내부 소켓의 동작에 대한 더 자세한 제어를 제공한다. 예를 들어, 다른 프록시 서버나 암호화 방법을 선택할 수 있다.


public Socket()

public Socket(Proxy proxy)

protected Socket(SocketImpl impl)



연결에 사용할 로컬 인터페이스 지정하기


다음 두 개의 생성자는 연결할 호스트의 포트 이외에도 연결에 사용할 인터페이스와 로컬 포트를 매개변수로 받는다.


public Socket(String host, int port, InetAddress interface, int localPort) 

throws IOException, UnknownHostException

public Socket(InetAddress host, int port, InetAddress interface, int localPort)

throws IOException


이 소켓 생성자는 마지막 두 개의 매개변수로 전달된 로컬 네트워크 인터페이스와 포트로부터, 처음 두 매개변수로 지정된 호스트와 포트로 접속한다. 네트워크 인터페이스는 이더넷 카드와 같은 물리적인 장치이거나, 하나 이상의 IP 주소를 가진 멀티 홈 호스트(multi-home-host)처럼 가상의 장치일 수 있다. localPort 인자로 0이 전달될 경우 자바는 1024에서 65535 사이의 사용 가능한 임의의 포트를 선택한다.


데이터를 보내기 위해 특정 인터페이스를 선택하는 경우는 일반적이지는 않지만, 가끔 필요한 경우가 있다.


로컬 주소를 명시적으로 지정해야 하는 상황의 한 가지 예로 이중(dual) 이더넷 포트를 사용하는 라우터/방화벽이 있다.라우터/방화벽은 하나의 인터페이스를 통해 외부의 연결을 받아들이고, 처리한 다음 다른 인터페이스를 통해 로컬 네트워크로 전달한다.


또한, 주기적으로 에러 로그를 프린터로 출력하거나 내부 메일 서버로 전송하는 프로그램을 작성한다고 가정해 보면,

해당 패킷이 외부 인터페이스가 아닌 내부 인터페이스로 전송되도록 다음과 같이 할 수 있다.


try {

InetAddress inward = InetAddress.getByName("router");

Socket socket = new Socket("mail", 25, inward, 0);

//소켓을 이용한 데이터를 보내거나 받는 작업들....

} catch (IOException ex) {

System.err.println(ex);

}


위 코드에서 로컬 포트 숫자로 0을 전달함으로써, 어떤 임의의 포트를 사용해도 괜찮으나, 로컬 호스트 네임 "router"에 연결된 네트워크 인터페이스를 사용할 것임을 요청했다.


이 생성자는 이전의 생성자와 같은 이유로 IOException 또는 UnknownHostException 예외를 발생시킨다. 이외에도 소켓이 요청된 로컬 네트워크 인터페이스를 바인드(bind) 할 수 없는 경우 IOException 예외를 발생시킨다. (이 메소드의 예외 조항에 구체적으로 명시되어 있지는 않지만, 아마도 IOException의 서브 클래스인 BindException이 발생할 것이다.)


예를 들어, a.example.com에서 실행 중인 프로그램은 b.example.org에서 접속할 수 없다. 이런 점을 이용하면 컴파일 된 프로그램이 미리 정해진 호스트에서만 실행되도록 의도적으로 제한할 수 있다. 이 방법은 각 컴퓨터에 맞게 설정된 배포가 필요하며, 확실히 값싼 제품에 대해 과도한 기능이다.


게다가 자바 프로그램은 쉽게 디스어셈블(disassemble), 디컴파일(decompile), 그리고 리버스 엔지니어(reverse engineer)할 수 있기 때문에, 이 구조가 확실히 안전하지는 않다. 그럼에도 불구하고 소프트웨어 라이선스를 적용하기 위한 방법으로 종종 사용된다.



연결되지 않은 소켓 생성하기


지금까지 이야기한 모든 생성자는 소켓 객체를 생성하고 원격 호스트에 대한 네트워크 연결을 여는 두 작업을 함께 수행한다. 가끔 이 두 작업을 따로 수행해야 할 필요가 있다. Socket 생성자를 아무런 인자 없이 호출하면 생성자는 연결되지 않은 소켓을 반환한다.


public Socket()


소켓 생성 이후에 SocketAddress를 매개변수로 connect() 메소드 중 하나를 호출하여 연결할 수 있다.


예를 들어,


try{

Socket socket = new Socket();

//소켓 옵션 입력

SocketAddress address = new InetSocketAddress("time.nist.gov", 13);

socket.connect(address)

//연결된 소켓으로 작업...

}catch (IOException ex) {

System.err.println(ex);

}


connect() 메소드의 두 번째 인자로 연결 타임아웃 시간을 밀리초 단위로 설정할 수 있다.


public void connect(SocketAddress endpoint, int timeout) throws IOException


기본 타임아웃 값은 0이며, 무한히 대기한다.


이 생성자는 기본적으로 다른 종류의 소켓을 사용하기 위해 존재하며, 또한 소켓 연결 이전에 변경해야 적용되는 옵션을 설정할 때 필요하다. 이 생성자의 가장 큰 장점은 try-catch-finally 블록에서 코드를 깔끔하게 정리할 수 있다는 것이다. 매개변수가 없는 생성자는 예외를 발생시키지 않기 때문에 finally 블록에서 소켓을 닫을 때 불필요한 널(null) 체크를 하지 않아도 된다.


매개변수가 필요한 생성자를 호출할 경우 대부분 아래와 같이 코딩한다.


Socket socket = null;

try{

socket = new Socket(SERVER, PORT);

// 소켓을 이용한 작업...

}catch (IOException ex) {

System.err.println(ex);

}finally {

if (socket != null) {

try{

socket.close();

}catch (IOException ex) {

// 무시한다

}

}

}


매개변수가 없는 생성자를 호출할 경우에는 다음과 같이 코딩할 수 있다.


Socket socket = new Socket();

SocketAddress address = new InetSocketAddress(SERVERPORT);

try{

socket.connect(address);

// 소켓을 이용한 작업...

}catch (IOException ex){

System.err.println(ex);

}finally {

try{

socket.close();

catch (IOException ex) {

// 무시한다

}

}