✅ 정리
서버와 클라이언트에
소켓 둘다 만든다음에 스트림으로 통신한다
서버는 서버 소켓을 열어둠
클라이언트가 먼저 어떤 포트의 서버에 접속을 시도하면
TCP 3 way handshake 가 발생하고, tcp 연결이 완료된다
tcp 연결이 완료되면 서버는 Os backlog quequ 라는 곳에 클라이언트와 서버의 tcp 연결 정보를 보관
하지만 이시점에서는 클라이언트의 소켓 객체가 정상 생성되서 잘 연결이 되었지만
아직 서버의 소켓 객체는 생성되지 않음
서버에는 따로 서버소켓이 있음
지정한 포트를 사용해서 서버 소켓을 생성하면, 클라이언트는 해당 포트로 서버에 연결할 수 있다
서버에서 accept()
호출하면 연결정보 기반으로 소켓 객체를 생성
그 만들어진 소켓 객체를 통해 클라이언트와 서버가 통신함
서버소켓은 클라이언트가 서버에 접속할 수 있는것까지만 해주고 + (접속 정보를 백로그에 담아줌)
서버소켓으로 만들어진 소켓이 실제 클라이언트와 1:1 통신을 하는 것임 (스트림을 통해)
별도의 Sesison 객체를 별도의 스레에서 생성해서 클라이언트에 연결한다
✅ 자원 정리
서버를 오래 켜두고 자원을 정리하지 않으면 자주 서버가 다운되는 현상이 발생함
try-with-resource
사용법 : AutoCloseable
를 implements
해야함
try-with-resource
장점
close()
호출이 필요 없어짐try
블럭 안으로 한정된다try -> catch -> finally
로 catch
이후에 자원을 반납try-with-resource
는 try
블록이 끝나면 즉시 close()
를 호출해서 자원을 반납try-with-resource
단점
try
선언부에서 사용한 자원만 정리할 수 있다예시
public class ResourceV2 implements AutoCloseable{
// try-with-resource 사용하려면 AutoCloseable 구현해야함
private String name;
public ResourceV2(String name) {
this.name = name;
}
public void call() {
System.out.println(name + " call");
}
public void callEx() throws CallException {
System.out.println(name + " callEx");
throw new CallException(name+ " ex");
}
public void close() throws CloseException {
System.out.println(name + " close");
throw new CloseException(name + " ex");
}
}
✅ 셧다운 혹
프로세스 종료 종류에는 정상 종료
와 강제 종료
두가지가 있다
✅ 정상 종료
- 과정
클라이언트가 서버에 연결되있다
서버가 연결 종료를 위해 socket.close()
호출 = 서버는 클라이언트에 FIN
패킷을 보낸다
클라이언트는 FIN
패킷을 받는다
서버가 소켓을 종료했다는 의미는 클라이언트는 더는 읽은 데이터가 없다는 뜻
FIN
패킷을 받은 클라이언트의 소켓은 더는 서버를 통해 읽을 데이터가 없다는 의미로
EOF 또는 -1 또는 null
반환
FIN
메시지를 받은 클라이언트도 close()
를 호출해서 서버에 FIN
메시지를 보내고
소켓 연결을 끊어야함
클라이언트의 운영체제에서 자동으로 FIN
에 대한 ACK
패킷을 서버에 전달
클라이언트 종료를 위해 socket.close()
호출 = 클라이언트는 서버에 FIN
패킷을 보낸다
서버는 FIN
패킷을 받는다
서버의 운영체제에서 자동으로 FIN
패킷에 대한 ACK
패킷을 클라이언트에 전달
이렇게 서로 FIN
메시지를 주고 받으면서 TCP 연결이 정상 종료
된다
// 서버클래스
public class NormalCloseServer {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket = new ServerSocket(12345);
Socket socket = serverSocket.accept();
log("소캣 연결: " + socket);
Thread.sleep(1000);
// 상대방이 연결을 종료한 경우
socket.close(); // 클라이언트에 FIN 패킷보냄
log("소캣 종료");
}
}
// 클라이언트 클래스
public class NormalCloseClient {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("localhost", 12345);
log("소캣 연결: " + socket);
InputStream input = socket.getInputStream();
readByDataInputStream(input, socket);
log("연결 종료: " + socket.isClosed());
}
private static void readByDataInputStream(InputStream input, Socket socket) throws IOException {
DataInputStream dis = new DataInputStream(input);
try {
dis.readUTF(); // 읽을 데이터 없으면 EOFEXception 예외 발생
} catch (EOFException e) {
log(e);
} finally {
dis.close();
socket.close(); // 서버에 FIN 패킷 보냄
}
}
}
✅ 강제 종료
RST
라는 패킷이 발생하며 이 경우 즉시 종료해애 한다RST
패킷은 RESET 의 줄임말로 연결 상태를 초기화해서 더 이상 연결을 유지하지 않겠다는 의미RST
패킷을 받으면 더 이상 요청을 보내면 안됨 (연결을 종료하라는 의미)
- 과정
클라이언트와 서버가 연결되어 있다
서버는 종료를 위해 socket.close()
를 호출 = 서버는 클라이언트에 FIN
패킷을 전달
클라이언트는 FIN
패킷을 받는다
클라이언트의 운영체제에서 FIN
에 대한 ACK
패킷을 서버에 전달
원래는 정상적으로 연결을 끊으려면 클라이언트에서도 서버에 FIN
패킷을 보내야하는데
다음과 같이 PUSH
패킷을 서버에 전달하면 서버측에서는 TCP 연결에 문제가 있다고 판단해서
즉각 연결을 종료하라는 RST
패킷을 클라이언트에 전달한다
RST 패킷이 도착했다는 것은 현재 TCP 연결에 심각한 문제가 있으므로 해당 연결을 더는 사용하면 안된다는 의미
하지만 이러한 네트워크의 예외 모두 처리하기는 실질적으로 어렵다
결론은 정상 종료
강제 종료
모두 자원을 정리하고 닫도록 설계하자
SocketException
EOFException
모두 IOException
의 자식으로 IOException
으로 잡으면 된다
✅ 타임아웃
타임아웃에는 TCP 연결 타임아웃
TCP 소켓 타임아웃(read 타임 아웃)
2가지 종류가 있다
연결 타임 아웃
은 네트워크 연결을 시도할 때 응답이 없으면 발생한다
다음 예제는 TCP 연결 타임 아웃
시간을 직접 설정하는 예제이다
public class ConnectTimeoutMain2 {
public static void main(String[] args) throws IOException {
try {
Socket socket = new Socket();
socket.connect(new InetSocketAddress("192.168.1.250", 45678), 1000);
// 1초 타임아웃 시간이 지나도 연결이 되지 않으면 SocketTimeoutException 발생
} catch (SocketTimeoutException e) {
// // java.net.SocketTimeoutException: Connect timed out
e.printStackTrace();
}
}
}
다음은 또 중요한 TCP 소켓 타임아웃(read 타임 아웃)
을 알아보자
이 타임아웃은 연결이 된 상태에서 클라이언트가 서버에 어떤 요청을 했을 때
서버에서 계속 응답을 주지 않으면 무한정 기다려야 하는 상황이 발생한다
다음 예제는 TCP 소켓 타임아웃
에 타임아웃을 설정하는 예제이다
public class SoTimeoutClient {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("localhost", 12345);
InputStream input = socket.getInputStream();
try {
socket.setSoTimeout(3000); // 타임아웃 시간 설정
int read = input.read(); // 기본은 무한 대기
System.out.println("read = " + read);
} catch (Exception e) {
e.printStackTrace();
}
socket.close();
}
}
예로들어 신용카드를 처리하는 회사가 3개 있다고 가정해보자
신용카드 A, 신용카드 B 서버는 문제가 없고
신용카드 C 회사 서버에 문제가 발생해서 응답을 주지 못하는 상황이라고 가정해보자.
주문 서버는 계속 신용카드 C 회사 서버의 응답을 기다리게 된다.
신용카드C의 결제에 대해서 주문 서버도 고객에게 응답을 주지 못하고 계속 대기하게 된다. 신용카드C로 주문하는 고객이 누적 될 수록 주문 서버의 요청은 계속 쌓이게 되고,
신용카드 C 회사 서버의 응답을 기다리는 스레드도 점점 늘어난다.
결국 주문 서버에 너무 많은 사용자가 접속하게 되면서 주문 서버에 장애가 발생하게 된다.
결과적으로 신용카드C 때문에 신용카드A, 신용카드B를 사용하는 고객까지 모두 주문을 할 수 없는 사태가 발생
이처럼 C회사 서버가 연결이 오래걸려서 응답을 주지 않을 때는 타임아웃으로 처리해야한다
결론은 외부 서버와 통신을 하는 경우 연결 타임아웃
과 소켓 타임아웃
은 지정해야한다