//network 연결시 예외
public class ConnectMain {
public static void main(String[] args) throws IOException {
unknownHostEx1();
unknownHostEx2();
connectionRefused();
}
//이상한 IP 접근
private static void unknownHostEx1() throws IOException {
try {
Socket socket = new Socket("999.999.999.999", 80);
}
catch (UnknownHostException e) {
e.printStackTrace();
}
}
//이상한 도메인 접근
private static void unknownHostEx2() throws IOException {
try {
Socket socket = new Socket("google.gogo", 80);
}
catch (UnknownHostException e) {
e.printStackTrace();
}
}
private static void connectionRefused() throws IOException {
try {
Socket socket = new Socket("localhost", 45678); // 미사용 포트
} catch (ConnectException e) {
// ConnectException: Connection refused
e.printStackTrace();
}
}
}
- Connection refused 메시지는 연결이 거절되었다는 뜻이다.
- 연결이 거절되었다는 것은, 우선은 네트워크를 통해 해당 IP의 서버 컴퓨터에 접속은 했다는 뜻이다.
- 그런데 해당 서버 컴퓨터가 45678 포트를 사용하지 않기 때문에 TCP 연결을 거절한다.
- IP에 해당하는 서버는 켜져있지만, 사용하는 PORT가 없을 때 주로 발생한다.
- 네트워크 방화벽 등에서 무단 연결로 인지하고 연결을 막을 때도 발생한다.
- 서버 컴퓨터의 OS는 이때 TCP RST(Reset)라는 패킷을 보내서 연결을 거절한다.
- 클라이언트가 연결 시도 중에 RST 패킷을 받으면 이 예외가 발생한다.
TCP RST(Reset) 패킷: TCP 연결에 문제가 있다는 뜻이다. 이 패킷을 받으면 받은 대상은 바로 연결을 해제해야 한다.
public class ConnectTimeoutMain1 {
//Windows: 약 21초
//Linux: 약 75초에서 180초 사이, mac test 75초
//java.net.ConnectException: Operation timed out
public static void main(String[] args) throws IOException {
long start = System.currentTimeMillis();
try {
Socket socket = new Socket("192.168.1.250", 45678);
} catch (ConnectException e) {
// ConnectException: Operation timed out
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + (end - start));
}
}
java.net.ConnectException: Connection timed out: connect
at java.base/sun.nio.ch.Net.connect0(Native Method)
at java.base/sun.nio.ch.Net.connect(Net.java:589)
at java.base/sun.nio.ch.Net.connect(Net.java:578)
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:583)
at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327)
at java.base/java.net.Socket.connect(Socket.java:751)
at java.base/java.net.Socket.connect(Socket.java:686)
at java.base/java.net.Socket.<init>(Socket.java:555)
at java.base/java.net.Socket.<init>(Socket.java:324)
at network.tcp.exception.connect.ConnectTimeoutMain1.main(ConnectTimeoutMain1.java:14)
end = 21071
사설 IP 대역(주로 공유기에서 사용하는 IP 대역)의 192.168.1.250 을 사용했다. 혹시 해당 IP로 무언가 연결 되어 있다면 다른 결과가 나올 수 있다. 이 경우 마지막 3자리를 변경해보자. 해당 IP로 연결 패킷을 보내지만 IP를 사용하는 서버가 없으므로 TCP 응답이 오지 않는다. 또는 해당 IP로 연결 패킷을 보내지만 해당 서버가 너무 바쁘거나 문제가 있어서 연결 응답 패킷을 보내지 못하는 경우도 있다. 그렇다면 어떻게 해결해야할까? TCP 연결을 시도했는데 연결 응답이 없다면 OS에는 연결 대기 타임아웃이 설정되어 있다.
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);
} catch (SocketTimeoutException e) {
// // java.net.SocketTimeoutException: Connect timed out
e.printStackTrace();
}
}
}
public void connect(SocketAddress endpoint, int timeout) throws IOException {...}
InetSocketAddress: SocketAddress 의 자식이다. IP, PORT 기반의 주소를 객체로 제공한다.
timeout: 밀리초 단위로 timeout 설정
연결 잘된 이후 문제
public class SoTimeoutServer {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket = new ServerSocket(12345);
Socket socket = serverSocket.accept();
Thread.sleep(1000000);
}
}
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();
}
}
점점 서버 응답을 기다리는 Thread가 늘어나게 되고 결국 서버 장애가 생기게 된다.
외부 서버와 통신을 하는 경우 반드시 연결 타임아웃과 소켓 타임아웃을 지정하자.
정상 종료
TCP에서 A, B가 서로 통신한다고 가정해보자. TCP 연결을 종료하려면 서로 FIN 메시지를 보내야 한다.
A (FIN) -> B: A가 B로 FIN 메시지를 보낸다.
A <- (FIN) B: FIN 메시지를 받은 B도 A에게 FIN 메시지를 보낸다.
socket.close() 를 호출하면 TCP에서 종료의 의미인 FIN 패킷을 상대방에게 전달한다. FIN 패킷을 받으면 상대방도 socket.close() 를 호출해서 FIN 패킷을 상대방에게 전달해야 한다.
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();
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();
readByInputStream(input, socket);
readByBufferedReader(input, socket);
readByDataInputStream(input, socket);
log("연결 종료: " + socket.isClosed());
}
private static void readByInputStream(InputStream input, Socket socket) throws IOException {
int read = input.read();
log("read = " + read);
if (read == -1) {
input.close();
socket.close();
}
}
private static void readByBufferedReader(InputStream input, Socket socket) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(input));
String readString = br.readLine();
log("readString = " + readString);
if (readString == null) {
br.close();
socket.close();
}
}
private static void readByDataInputStream(InputStream input, Socket socket) throws IOException {
DataInputStream dis = new DataInputStream(input);
try {
dis.readUTF();
} catch (EOFException e) {
log(e);
} finally {
dis.close();
socket.close();
}
}
}
- 클라이언트가 서버에 접속한다.
- 클라이언트는 input.read() 로 서버의 데이터를 읽기 위해 대기한다.
- 그런데 1초 뒤에 서버에서 연결을 종료한다.
- 서버에서 socket.close() 를 호출하면 클라이언트에 FIN 패킷을 보낸다.
- 클라이언트는 FIN 패킷을 받는다.
- 서버가 소켓을 종료했다는 의미이므로 클라이언트는 더는 읽을 데이터가 없다.
- FIN 패킷을 받은 클라이언트의 소켓은 더는 서버를 통해 읽을 데이터가 없다는 의미로 -1(EOF)를 반환한다.
EOF가 발생하면 상대방이 FIN 메시지를 보내면서 소켓 연결을 끊었다는 뜻이다. 이 경우 소켓에 다른 작업을 하면 안되고, FIN 메시지를 받은 클라이언트도 close() 를 호출하여 상대방에 FIN 메세지를 보내고 Socket connect를 끊어야 한다.
이렇게 서로 FIN 메세지를 주고 받으면서 TCP연결이 정상 종료된다.
강제종료
public class ResetCloseServer {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket serverSocket = new ServerSocket(12345);
Socket socket = serverSocket.accept();
log("소캣 연결: " + socket);
socket.close();
serverSocket.close();
log("소캣 종료");
}
}
public class ResetCloseClient {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket("localhost", 12345);
log("소캣 연결: " + socket);
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
// client <- server: FIN
Thread.sleep(1000); // 서버가 close() 호출할 때 까지 잠시 대기
// client -> server: PUSH[1] : 데이터 전송
output.write(1);
// client <- server: RST
Thread.sleep(1000); // RST 메시지 전송 대기
try {
int read = input.read();
System.out.println("read = " + read);
} catch (SocketException e) {
e.printStackTrace();
}
try {
output.write(1);
} catch (SocketException e) {
//java.net.SocketException: Broken pipe
e.printStackTrace();
}
}
}
- 클라이언트와 서버가 연결되어 있다.
- 서버는 종료를 위해 socket.close() 를 호출한다.
- 서버는 클라이언트에 FIN 패킷을 전달한다.
- 클라이언트는 FIN 패킷을 받는다.
- 클라이언트의 OS에서 FIN에 대한 ACK 패킷을 전달한다.
- 클라이언트는 output.write(1) 를 통해 서버에 메시지를 전달한다.
- 데이터를 전송하는 PUSH 패킷이 서버에 전달된다.
- 서버는 이미 FIN으로 종료를 요청했는데, PUSH 패킷으로 데이터가 전송되었다.
- 서버가 기대하는 값은 FIN 패킷이다.
- 서버는 TCP 연결에 문제가 있다고 판단하고 즉각 연결을 종료하라는 RST 패킷을 클라이언트에 전송한다.
RST 패킷이 도착했다는 것은 현재 TCP 연결에 심각한 문제가 있으므로 해당 연결을 더는 사용하면 안된다는 의미이다.
'컴퓨터 네트워크' 카테고리의 다른 글
Network program - 2 (0) | 2024.12.24 |
---|---|
Network program (0) | 2024.12.24 |
네트워크 - 기본 이론 (1) | 2024.12.24 |
InputStream, OutputStream (1) | 2024.12.22 |
컴퓨터 네트워크 4장- 라우터 내부 (0) | 2024.05.18 |