본문 바로가기

JAVA/네트워크 프로그래밍

[JAVA] 서버 소켓 사용하기 ①

위는 time-a.nist.gov에 연결하여 시간을 받아 온 것이다.



나만의 daytime 서버를 만들어보자.


우선 포트 13번에 대기하는 서버 소켓을 만든다.


ServerSocket server = new ServerSocket(13);


다음, 연결을 수용한다.


Socket connection = server.accept();


accept()가 호출되면 프로그램은 여기서 실행을 멈추고 클라이언트 포크가 13번으로 연결할 때까지 무한 대기 한다.


클라이언트가 연결되면 accept() 메소드는 Socket 객체를 반환한다.


반환된 연결은 java.net.Socket 객체 형태로 반환되며 클라이언트에서 사용한 것과 같다.


daytime 프로토콜은 대화할 서버가 필요하므로 소켓에서 OutputStream 을 구한다.


그리고 daytime 프로토콜은 텍스트 프로토콜을 사용하기 때문에 OutputStream을 OutputStreamWriter에 연결한다.


OutputStream out = connection.getOutputStream();

Writer writer = new OutputStreamWriter(out, "ASCII");


이제 현재 시간을 구하고 스트림에 시간을 쓴다.


Date now = new Date();

out.write(now.toString() + "\r\n");


라인을 끝내기 위한 캐리지리턴/라인피드 쌍(\r\n)의 사용법에 주의하도록 해야한다.


이 값은 위 코드처럼 명확하게 출력해야 한다. 명시적으로 시스템 라인 구분자(system line separator)를 출력하는 System.getProperty("line.separator")를 호출하는 방법이나 println()같은 은연중에 시스템 라인 구분자를 출력하는 메소드의 사용은 지양해야한다. 


마지막으로 연결을 플러시하고 닫는다.


out.flush();

connection.close();


항상 한 번 전송한 뒤에 바로 연결을 닫아야 하는 것은 아니다. dict와 HTTP 1.1과 같은 프로토콜에서 클라이언트는 단일 소켓을 통해 다수의 요청을 보내고 서버로부터 응답을 받을 수 있다. FTP 같은 몇몇 프로토콜은 소켓을 열린 상태로 무한히 잡고 있을 수 있다. 그러나 daytime 프로토콜은 단일 연결에 대해 한 번의 요청과 응답만이 허용된다.


서버가 여전히 동작 중일 때 클라이언트가 연결을 종료할 경우, 서버에서 클라이언트로 연결된 입출력 스트림은 다음 읽기 또는 쓰기 시에 InterruptedIOException 예외를 발생시킨다. 어떤 경우든 간에 서버는 다음에 들어올 연결을 처리할 준비를 해야한다.


물론 이 모든 작업들을 반복해서 처리하고 싶을 것이다. 그래서 이 코드 전체를 감싸는 루프를 추가할 수 있다. 루프는 반복마다 매번 accept() 메소드를 한 번씩 호출한다. 이 메소드는 원격 클라이언트와 로컬 서버 사이의 연결을 나타내는 Socket 객체를 반환한다. 클라이언트와의 통신은 이 Socket 객체를 통해 일어난다.


ServerSocket server = new ServerSocket(port);

while(true){

try {

Socket connection = server.accept();

Writer out = new OutputStreamWriter(connection.getOutputStream());

Date now = new Date();

out.write(now.toString()+"\r\n");

out.flush();

} catch (IOException e) {

System.err.println(e.getMessage());

}

}


큰 루프가 하나 있고 루프의 반복마다 단일 연결의 요청이 완벽하게 처리되는 이러한 구조를 반복 서버(iterative server)라고 부른다. 이 서버는 daytime 같은 작은 요청과 응답으로 구성된 간단한 프로토콜에 대해 매우 잘 동작한다.

하지만 이 서버 구조에는 느린 클라이언트 하나가 서버 전체를 느리게 만들 수 있는 잠재적인 문제가 있다. 뒤에 나올 예제들에서는 멀티스레드와 비동기 I/O를 사용하여 이러한 잠재적인 문제까지 해결한다.


ServerSocket server = null;

try {

server = new ServerSocket(port);

while(true) {

Socket connection = null;

try {

connection = server.accept();

Writer out = new OutputStreamWriter(connection.getOutputStream());

Date now = new Date();

out.write(now.toString() + "\r\n");

out.flush();

connection.close();

} catch (IOException ex) {

// 연결만 된 경우, 무시한다

} finally {

try {

if (connection != null) connection.close();

} catch (IOException ex) {}

}

}

catch (IOException ex) {

ex.printStackTrace();

finally {

try {

if (server != null) server.close();

catch (IOExcpetion ex) {}

}


소켓 사용이 끝나면 항상 닫아 줘야 한다. 서버는 예상치 못한 여러 상황들에 대비하여야 하므로, 클라이언트에 의존하여 소켓을 종료하는 것은 바람직하지 않다.


아래 예제는 위의 모든 내용을 포함하고 있으며, 소켓을 자동으로 닫기 위해 try-with-resources 구문을 사용하였다.


package network;


import java.io.IOException;

import java.io.OutputStreamWriter;

import java.io.Writer;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.Date;


public class T51DaytimeServer {

public final static int PORT= 13;

public static void main(String[] args) {

try(ServerSocket server = new ServerSocket(PORT)){

while(true){

try{

Socket connection = server.accept();//새로운 연결을 감시하기 위함

Writer out = new OutputStreamWriter(connection.getOutputStream());

Date now = new Date();//현재시간을 구하는 데 사용

out.write(now.toString()+"\r\n");//out의 write() 메소드로 클라이언트에 전송

out.flush();

connection.close();

} catch (IOException e) {

System.err.println(e);

}//연결을 수용하고 처리할 때 발생하는 예외 처리

}

}catch (IOException ex){

System.err.println(ex);

}//daytime 포트에 ServerSocket 객체 server가 생성될 때 발생하는 예외 처리

}

}

텔넷으로 이 예제에 연결한 결과. 


이 클래스는 단일 메소드인 main()에서 모든 일을 처리한다. 


일반적인 많은 서버들처럼 이 프로그램은 종료되지 않으며, 예외가 발생하거나 수동으로 프로그램을 멈출 때까지 계속해서 대기하고 있다.


클라이언트가 연결되면 accept() 메소드는 Socket 객체를 반환하고, 이 객체는 로컬 변수 connection에 저장된다.


그리고 블로킹되어 있던 프로그램은 계속해서 실행된다. 다음으로 Socket과 연결된 출력 스트림을 얻기 위해 getOutputStream() 메소드를 호출하고 출력 스트림을 OutputStreamWriter에 연결하여 out을 생성한다.