본문 바로가기

JAVA/네트워크 프로그래밍

[JAVA] 소켓으로 서버에 쓰기(Client)

소켓에 입력 스트림뿐만 아니라 출력 스트림 또한 요청할 수 있다. 


입력 스트림을 통해 데이터를 읽고 있는 동시에 출력 스트림을 사용하여 동시에 데이터를 보내는 것이 가능하지만, 대부분의 프로토콜은 동시에 읽고 쓰지 않도록 설계되어 있다.


즉, 한 번에 읽거나 쓰는 하나의 동작만 수행한다.


일반적으로 클라이언트가 요청을 보내면, 요청을 받은 후 서버가 응답을 보낸다. 그러고 나서 클라이언트가 또 다른 요청을 보내면, 서버는 또 다른 요청을 받은 후 응답을 보낸다.


이 과정은 어느 한쪽이 종료되거나 연결이 닫힐 때까지 계속된다.



양방향 TCP 프로토콜을 사용하는 간단한 예로 RFC 2229에 정의된 dict가 있다. 


이 프로토콜에서 클라이언트는 dict 서버의 2628 포트에 대해 소켓을 열고 "DEFINE eng-lat gold" 와 같은 명령을 보낸다. 


이 명령은 서버에게 영어/라틴어 사전에 있는 "gold"의 정의를 요청한다. (서버마다 다른 사전이 설치되어 있을 수 있음)


클라이언트는 요청에 대한 응답을 받은 후에 다른 단어를 다시 요청할 수 있고, 더이상 요청할 단어가 없는 경우 "quit"를 보내 연결을 종료한다.


텔넷을 사용하여 아래와 같이 dict 프로토콜을 살펴 볼 수 있다.



위 결과에서 세 자리 숫자 코드로 시작하는 제어 응답 라인(control response line)을 볼 수 있다.


실제 단어의 정의 내용은 일반적인 텍스트 문장으로 출력되며, 마침표로 구성된 라인으로 종료된다.



자바에서 이 프로토콜을 어렵지 않게 구현할 수 있다. 먼저 dict 서버에 대한 소켓을 연다.


Socket socket = new Socket("dict.org", 2628);


그리고 또, 서버가 응답이 없는 경우를 대비하여 타임아웃을 설정할 수 있다.


socket.setSoTimeout(15000);


dict 프로토콜은 클라이언트가 먼저 요청하기 때문에, getOutputStream()을 사용하여 출력 스트림을 요청한다.


OutputStream out = socket.getOutStream();


getOutputStream() 메소드는 여러분의 프로그램에서 소켓의 반대편에 있는 프로그램으로 데이터를 쓰기 위한 원시 OutputStream을 반환한다.


그리고 보통 이 스트림을 사용하기 전에 DataOutputStream이나 OutputStreamWriter같은 좀 더 쓰기 편한 클래스와 연결한다.


성능상의 이유로 버퍼링 또한 하는 것이 좋다. Dict 프로토콜이 텍스트(UTF-8) 기반이기 때문에 Writer로 감싸는 것이 좀 더 편리하다.


Writer wirter = new OutputStream(out, "UTF-8");


이제 소켓을 통해 다음과 같이 명령을 전송할 수 있다.


writer.write("DEFINE fd-eng-lst gold\r\n");


마지막으로, 플러시를 호출하여 네트워크를 통해 명령을 확실히 전송할 수 있다.


writer.flush();


그러면 이제 서버는 단어의 정의를 포함한 응답을 보낼 것이고, 소켓의 입력 스트림을 사용하여 서버의 응답을 읽을 수 있다.


InputStream in = socket.getInputStream();

BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));

for(String line = reader.readLine() ; !line.equals(".") ; line=reader.readLine()){

System.out.println(line);

}


마침표로 구성된 라인을 읽게 되면 단어의 정의가 끝났음을 알 수 있다. 그 이후에 출력 스트림을 통해 "quit" 명령을 보낼 수 있다.


writer.write("quit\r\n");

writer.flush();



이제 아래의 예제는 완전한 dict 클라이언트 프로그램이다.


이 프로그램은 dict.org 서버로 연결하고, 사용자가 명령라인으로 입력한 단어를 라틴어로 번역한다.


이 프로그램은 150또는 220과 같은 응답코드로 시작하는 모든 메타데이터 라인을 걸러낸다.


그러나 서버가 요청된 단어를 인지하지 못하는 경우는 "552 no match"로 시작하는 라인은 특별히 처리한다.


package network;


import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.io.OutputStream;

import java.io.OutputStreamWriter;

import java.io.Writer;

import java.net.Socket;


public class T06DictClient {

public static final String SERVER = "dict.org";

public static final int PORT = 2628;

public static final int TIMEOUT = 15000;

public static void main(String[] args) {

Socket socket = null;

try{

socket = new Socket(SERVER PORT );

socket.setSoTimeout(TIMEOUT );

OutputStream out = socket.getOutputStream();

Writer writer = new OutputStreamWriter(out, "UTF-8");

writer = new BufferedWriter(writer);

InputStream in = socket.getInputStream();

BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));

for(String word: args){

define(word, writer, reader);

}

writer.write("quit\r\n");

writer.flush();

}catch(IOException ex){

System.err.println(ex);

}finally{//리소스해제(dispose)

if(socket != null){

try{

socket.close();

}catch(IOException ex){//무시한다}

}

}

}

}

static void define(String word, Writer writer, BufferedReader reader) throws IOException{

writer.write("DEFINE fd-eng-lat "+ word + "\r\n");

writer.flush();

for(String line = reader.readLine(); line != null; line = reader.readLine()){

if(line.startsWith("250 ")){//OK

return;

}else if(line.startsWith("552 ")){//일치하지 않음

System.out.println("No definition found for " + word);

return;

}

else if(line.matches("\\d\\d\\d .*")) continue;

else if(line.trim().equals(".")) continue;

else System.out.println(line);

}

}

}



이 예제는 라인 중심으로 동작한다. 이 프로그램은 콘솔로부터 한 라인을 입력받고, 이 내용을 서버로 보낸다.

그리고 서버로부터의 응답을 한 라인씩 읽기 위해 기다린다.

실행 결과

Dict 서버에 접속하여 gold platinum silver copper를 라틴어로 검색하였다.


한 쪽이 닫힌 소켓


close() 메소드는 소켓의 입력과 출력을 모두 닫는다.


때떄로 연결의 입력이나 출력 중에 어느 하나만 닫고 싶은 경우가 있다.


shutdownInput() 그리고 shutdownOutput() 메소드는 연결의 어느 한쪽만 닫는 데 사용된다.


public void shutdownInput() throws IOException

public void shutdownOutput() throws IOException


 이 두 메소드 중 어느 것도 실제로 소켓을 닫지는 않는다. 대신 스트림이 끝에 도달한 것 처럼 보이도록 소켓에 연결된 스트림을 조정한다.


입력을 닫은 이후에 입력 스트림에서 읽기를 시도할 경우 -1이 반환된다. 출력을 닫은 이후에 소켓에 추가적인 쓰기를 시도할 경우 IOException 예외가 발생한다.


finger, whois 그리고 HTTP와 같은 많은 프로토콜이 클라이언트가 서버로 요청을 보내는 것으로 시작한다.


그러고 나서 클라이언트는 서버가 보낸 응답을 읽는다. 상황에 따라 클라이언트가 요청을 보내고 난 다음에 더 이상 전송할 내용이 없을 경우 출력 스트림을 닫는 것도 가능할 것이다.


예를 들어, 다음 코드는 HTTP 서버에 요청을 보낸 다음 더 이상 쓸 필요가 없으므로 출력을 닫는다.


try (Socket connection = new Soeckt("www.oreilly.com", 80)) {

writer out = new OutputStreamWriter(connection.getOutputStream(), "8859_1");

out.writer("GET / HTTP 1.0\r\n\r\n");

out.flush();

connection.shutdownOutput();

//서버로부터 응답 읽기...

}catch (IOException ex) {

ex.printStackTrace();

}


소켓의 입력 또는 출력이 어느 한쪽을 닫았거나 양쪽 모두 닫은 경우에도 소켓의 사용이 끝나면 close() 메소드를 호출하여 명시적으로 소켓을 종료시켜야 한다는 사실을 명심해야한다.


소켓의 한쪽을 닫는 메소드는 단지 소켓의 스트림에만 영향을 줄 뿐 소켓에는 영향을 주지 않으며, 소켓의 포트와 같은 사용한 리소스를 해제하지 않는다.


inInputShutdown() 그리고 isOutputShutdown() 메소드는 각각 출력 스트림과 입력 스트림이 닫혔는지 열렸는지 유무를 반환한다.


소켓에 데이터를 읽거나 쓸 수 있는지 좀 더 구체적인 확인이 필요할 때 isConnected() 또는 isClosed() 보다 이 메소드를 사용할 수 있다.


public boolean isInputShutdown()

public boolean isOutputShutdown()