[ 서버 소켓(ServerSocket) ]
- 서버에서 클라이언트의 연결 요청을 받기 위해 만들어두는 일종의 문(Door)
- 포트를 지정해 해당 서비스의 호수를 지정함 (여러 개 문이 있을 때 어느 문으로 들어가야할 지)
[ 소켓(Socket) ]
- 네트워크를 통해 TCP/IP 통신을 연결시켜주는 객체
- 클라이언트와 서버가 연결된 상태를 만들어줌 (Session을 맺음)
자바에서 파일을 다룰 때 일단 파일을 열어서 파일 객체를 생성한 뒤 스트림이나 채널을 열어 input, output 작업을 수행하고 파일을 닫아줍니다. 네트워크 통신도 파일과 같다고 생각하면 됩니다. 소켓을 통해 서버-클라이언트 간 통신을 열어주는 것, 즉 세션을 맺어준 뒤 역시 스트림이나 채널을 열어 input, output 작업을 수행하면 됩니다.
서버 소켓은 소켓 생성 전 서버에서 클라이언트의 응답을 기다릴 수 있도록 서비스 포트를 생성해서 대기 상태로 두는 작업입니다. 서버를 하나의 커다란 집이라고 생각해보면 일단 집 주소는 하나의 IP가 됩니다. 각 방마다 서비스가 달라 어디는 식당이고 어디는 책방이고 한다면 클라이언트가 이 집(IP)에 방문했을 때 어느 방으로 찾아가야할지 알 수가 없습니다. 서비스 포트란 이 방문의 번호를 의미합니다. "식당을 찾아가려면 15000번 문으로 가시오" 라고 하는 것과 같습니다. 서버 소켓은 이 서비스 포트를 생성해서 문을 만들어줍니다.
[ 서버 소켓 생성 및 데이터 주기 ]
먼저 간단히 서버에서 소켓을 생성하고 클라이언트가 접속 요청 시 접속이 완료됐다는 메세지를 주는 코드를 짜보겠습니다. 이전에 다룬 기본 스트림과 버퍼 스트림을 써서 클라이언트로 데이터를 보내주는 출력 로직만 넣었습니다. 주의할 점은 버퍼 필터 클래스의 출력(write)은 실제 출력이 아닌 버퍼로 먼저 출력하는 것이라는 점입니다. 따라서 버퍼에 쌓인 내용을 다시 실제 출력해주는 것(flush)가 필요합니다.
ServerSocket 클래스의 accept() 메소드는 클라이언트의 연결 요청이 들어올 때까지 스레드를 블록킹(BLOCKING)하고 기다립니다. 연결 요청이 들어오면 연결된 Socket 객체를 반환합니다. 여기까지만 하면 ServerSocket의 역할은 종료됩니다. 코드에서는 try-with-resources 구문으로 괄호안에서 생성한 자원은 코드가 끝나면 자동 해제되지만, 만약 try-catch-finally 구문을 사용한다면 사용이 끝난 소켓은 자원을 해제해줘야 합니다. 서버 소켓은 서비스가 종료될 때까지 해제할 일이 거의 없겠지만 클라이언트와 연결된 소켓은 사용을 다하면 해제를 해줘야 합니다. 파일 닫기와 마찬가지로 Socket 클래스의 close() 메소드를 사용해주면 됩니다.
package hs;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) {
// 서버 소켓을 생성해서 대기 상태로 만듦
// 클라이언트에서 연결 요청이 오면 소켓을 생성해서 열결시킴
try(ServerSocket server = new ServerSocket(15000);
Socket socket = server.accept()) {
// 연결이 되었으므로 데이터를 전송해줄 스트림 통로를 생성함
OutputStream out = socket.getOutputStream();
// 속도 향상을 위해 버퍼 필터 스트림을 사용함 (선택사항)
BufferedOutputStream bufOut = new BufferedOutputStream(out);
// 연결되면 보낼 데이터
String sendData = "서버 접속 완료됐슴다.";
// 버퍼에 먼저 내용을 넣음
// (바로 출력하는 것이 아님에 주의, 버퍼에 출력하는 것)
bufOut.write(sendData.getBytes());
// 버퍼에 있는 내용을 실제로 출력함
bufOut.flush();
} catch (IOException e) {
System.out.println("연결에 실패했습니다.");
}
// try-catch-finally를 사용했다면 socket.close()로 소켓연결을 닫아줘야함
}
}
[ 클라이언트 소켓 생성 및 서버에서 데이터 받기 ]
이번에는 서버에 연결 요청을 해서 데이터를 받아오는 클라이언트 코드입니다. 서버 소켓이 필요 없다는 점만 다르고 나머지는 다 똑같습니다.
package hs;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
// 서버와 소켓 연결 성공하면 데이터를 받아올 InputStream 생성
try(Socket socket = new Socket("172.30.1.29", 15000);
InputStream in = socket.getInputStream()) {
// 속도 향상을 위해 필터 스트림 사용 (선택사항)
BufferedInputStream bufIn = new BufferedInputStream(in);
// 받은 데이터를 저장해줄 바이트 배열
byte[] dataFromServer = new byte[100];
// 데이터를 읽어옴
bufIn.read(dataFromServer);
// 받은 데이터 출력
System.out.println(new String(dataFromServer));
} catch (IOException e) {
System.out.println("서버 연결에 실패했습니다.");
}
}
}
이제 실행 결과를 확인해보겠습니다. 먼저 서버쪽을 먼저 실행해보면 클라이언트 연결 요청이 없어서 그냥 아무것도 뜨지 않고 응답 상태로 기다리고 있습니다.
현재 연결 상태를 확인해보기 위해 cmd에 들어가서 < netstat -an | find "15000" > 명령어를 입력합니다. netstat은 현재 컴퓨터에 연결된 네트워크 통신 정보를 보여주는 명령어이고, find는 그 중 포함되는 문자만 검색해서 보여줍니다.
실행하기 전에는 15000번의 포트 정보가 전혀 없으므로 아무것도 뜨지 않습니다.
실행 후에는 15000번 포트가 생성되어 연결 요청을 기다리고 있으므로 LISTENING 상태로 포트가 열립니다.
클라이언트를 실행하면 아래와 같이 정상적으로 서버에서 데이터를 받아와 출력해줍니다.
실행이 완료되고 다시 netstat을 확인해보면 연결이 끝난 상태인 TIME_WAIT로 바껴있습니다. 이 상태에서 시간이 지나면 목록에서 사라집니다. 만약 계속 연결된 상태라면 ESTABLISHED로 나옵니다. 서비스 체크할 때 세션이 잘 맺어지는지를 확인하는 방법으로 자주 쓰이는 방법입니다. 왼쪽은 "서버 IP : 포트" 이며 뒷쪽은 "클라이언트IP : 포트"입니다. 클라이언트의 포트는 별도로 지정하지 않는 이상 자동으로 생성됩니다.
간단한 응용으로 웹서버를 하나 만들어보겠습니다. 웹 구동을 잘 몰라서 간단하게만 작성해봤습니다. 클라이언트가 웹으로 접속하면 열줄의 글을 출력한 뒤, 클라이언트가 접속을 종료하면 메세지 출력 후 해당 소켓을 종료시키는 방식입니다.
IO의 스트림은 블로킹 모드로 작동하기 때문에 스레드풀을 사용해 각 클라이언트 접속시 별도 스레드를 사용해서 내용을 출력해주는 것으로 했습니다. NIO를 이용해 논블로킹 모드로 서버를 구성하는 방법은 다음글을 참조하시면 됩니다.
2020/02/04 - [JAVA/기본 문법] - 네트워크_소켓(Socket) 통신_NIO 입출력 [2/3]
package hs;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
// 스레드풀 생성
ExecutorService threadPool = Executors.newCachedThreadPool();
// 서버 소켓 생성, 8080포트 오픈
try (ServerSocket serverSoket = new ServerSocket(15000)) {
// 다수 클라이언트 접속을 위해 루프 구성
while (true) {
// 통신 연결, 실패 시 메세지 출력 후 반복문 재실행
try {
Socket socket = serverSoket.accept();
// 새로운 스레드로 수행
System.out.println(socket.getPort()
+ " 클라이언트가 연결되었습니다.");
threadPool.execute(new Page(socket));
} catch (IOException soketException) {
System.out.print("클라이언트-서버 연결 실패 : ");
soketException.printStackTrace();
continue;
}
}
} catch (IOException serverSoketException) {
serverSoketException.printStackTrace();
}
}
}
// 웹페이지 띄우는 클래스
class Page implements Runnable {
Socket socket;
// 생성자
Page(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
// output 스트림 및 버퍼 스트림 생성
try (BufferedOutputStream bufOut =
new BufferedOutputStream(socket.getOutputStream())) {
// 웹브라우저의 서비스 요청에 대해 응답해줘야 하는 구문
bufOut.write("HTTP/1.0 200 OK\n".getBytes());
bufOut.flush();
bufOut.write("Content-Type: text/html\n\n".getBytes());
bufOut.flush();
for (int i = 0; i < 10; i++) {
bufOut.write("<h1>웹서버에 접속하였습니다.</h1>".getBytes());
bufOut.flush();
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("스트림 생성 실패");
} finally {
try {
socket.close();
System.out.println(socket.getPort()
+ " 접속이 종료됐습니다.");
} catch (IOException e2) {
e2.printStackTrace();
}
}
}
}
'■ JAVA > Study' 카테고리의 다른 글
[JAVA] 네트워크_소켓(Socket) 통신_NIO 입출력(논블로킹) [2/3] ★★ (0) | 2021.03.06 |
---|---|
[JAVA] 비동기/동기, 블로킹/논블로킹 (0) | 2021.03.05 |
[JAVA] 예외처리 ★ (0) | 2021.03.04 |
[JAVA] 스레드 (0) | 2021.03.04 |
[JAVA] File 내용 ★★ (참고 - NIO) (0) | 2021.03.04 |