package chap56.util;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public abstract class MyLogger {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SS");
public static void log(Object obj) {
String time = LocalDateTime.now().format(formatter);
System.out.printf("%s [%9s] %s\n", time, Thread.currentThread().getName(), obj);
}
}
"Hello" -> 문자를 서버에 전달 -> 클라이언트 요청에 "World!"
package chap56.network.tcp.v1;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import static chap56.util.MyLogger.log;
public class ClientV1 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
log("Client started");
Socket socket = new Socket("localhost", PORT);
// 외부에 데이터 받을 수 있고
DataInputStream input = new DataInputStream(socket.getInputStream()); // 보조 스트림 사용 socket.getInputStream()
// 외부에 데이터를 보낼 수 있고
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
log("socket connected" + socket);
// 서버로부터 메시지 송신
String toSend = "Hello! ";
output.writeUTF(toSend);
log("client -> server: " + toSend);
// 서버로부터 메시지 수신
String received = input.readUTF();
log("server -> client: " + received);
// 자원 정리
log("연결 종료: " + socket);
input.close();
output.close();
socket.close();
}
}
/**
* 20:51:48.05 [ main] Client started
* 20:51:48.06 [ main] socket connectedSocket[addr=localhost/127.0.0.1,port=12345,localport=52296]
* 20:51:48.06 [ main] client -> server: Hello!
* 20:51:48.07 [ main] server -> client: Hello! Hello, Client!
* 20:51:48.07 [ main] 연결 종료: Socket[addr=localhost/127.0.0.1,port=12345,localport=52296]
*
* port=12345,localport=52296 : 12345 포트로 접속을 했고, 52296 포트로 접속을 받았다.
*
* 127.0.0.1 : loopback address
*/
package chap56.network.tcp.v1;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import static chap56.util.MyLogger.log;
public class ServerV1 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
log("Server started");
ServerSocket serverSocket = new ServerSocket(PORT); //12345 포트가 떠있음
log("Server socket created: " + PORT);
Socket socket = serverSocket.accept(); // 12345 포트로 들어오면 갖고 socket을 만들어줌
log("Client connected: " + socket);
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
// 클라이언트로부터 메시지 수신 (서버입장에서는 받는 입장)
String received = input.readUTF();
log("client -> server: " + received);
// 클라이언트로 부터 문자 송신 (서버입장에서는 보내는 입장)
String toSend = received + "Hello, Client!";
output.writeUTF(toSend);
log("server -> client: " + toSend);
// 자원 정리
log("연결 종료: " + socket);
input.close();
output.close();
socket.close();
serverSocket.close();
}
}
/**
* > Task :chap56.network.tcp.v1.ServerV1.main()
* 20:52:32.54 [ main] Server started
* 20:52:32.54 [ main] Server socket created: 12345
* 20:52:40.97 [ main] Client connected: Socket[addr=/127.0.0.1,port=52307,localport=12345] port 52307에 연결 되어 있음
* 20:52:40.97 [ main] client -> server: Hello!
* 20:52:40.97 [ main] server -> client: Hello! Hello, Client!
* 20:52:40.97 [ main] 연결 종료: Socket[addr=/127.0.0.1,port=52307,localport=12345]
*
*
* 서로 통신 하기 위해서는 IP & PORT가 필요하다. 클라이언트가 접근을 할때 남는 Port 하나 할당
*/
Localhost, 127.0.0.1"
localhost는 현재 사용중인 컴퓨터 자체를 가리킴
localhost는 127.0.0.1 이라는 IP로 Mapping
127.0.0.1은 IP 주소 체계에서 루프백 주소(loopback address)로 지정된 특별한 IP 주소
package chap56.network.tcp.v1;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* InetAddress 클래스는 IP 주소를 다루는 클래스
* InetAddress.getByName("호스트명") : IP 주소 얻어옴
*/
public class InetAddressMain {
public static void main(String[] args) throws UnknownHostException {
InetAddress localhost = InetAddress.getByName("localhost");
System.out.println("localhost = " + localhost);
InetAddress google = InetAddress.getByName("google.com");
System.out.println("google = " + google);
}
}
InetAddress 클래스는 IP 주소를 다루는 클래스
InetAddress.getByName("호스트명") : IP 주소 얻어옴
Socket socket = new Socket("localhost", PORT)
연결이 성공적으로 완료되면 Socket 객체를 반환
Socket은 서버와 연결되어 있는 연결점
Socket 객체를 통해 서버와 통신 가능

ServerSocket serverSocket = new ServerSocket(PORT);
서버 소켓이라는 특별한 소켓 사용

서버 12345 포트로 서버 소켓 Open, 클라이언트는 12345 포트로 서버에 접속
클라이언트가 12345 포트에 연결을 시도
OS 계층에서 TCP 3 way handshake 발생, TCP 연결 완료
TCP 연결이 완료되면 서버는 OS backlog queue라는 곳에 클라이언트와 서버의 TCP 연결 정보를 보관
TCP 연결시에는 클라이언트 서버 모드 IP 포트 정보 필요
서버의 경우 포트가 명확하게 지정되어 있어야 한다. 그래야 클라이언트에서 서버에 어떤 포트에 접속할지 알 수 있다. 반면에 서버에 접속하는 클라이언트의 경우에는 자신의 포트가 명확하게 지정되어 있지 않아도 된다. 클라이언트는 보통 포트를 생략하는데, 생략할 경우 클라이언트 PC에 남아 있는 포트 중 하나가 랜덤으로 할당
Socket socket = serverSocket.accept();
서버 소켓은 단지 클라이언트와 서버의 TCP 연결만 지원하는 특별한 소켓
실제 클라이언트와 서버가 정보를 주고 받으려면 Socket 객체 필요
serverSocket.accept() 호출 -> TCP 연결 정보 기반으로 Socket 객체 만들어서 반환

accept 호출시 backlog queue에서 TCP 연결 정보 조회
-> TCP 연결 정보 X, 연결 정보가 생성될 때 까지 대기 (블로킹)
해당 정보를 기반으로 Socket 객체 생성

클라이언트와 서버의 Socket은 TCP로 연결, 스트림을 통해 메시지 주고 받을 수 있음
마지막으로 자원 정리 꼭 해야 함
package chap56.network.tcp.v2;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
import static chap56.util.MyLogger.log;
public class ClientV2 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
log("Client started");
Socket socket = new Socket("localhost", PORT);
// 외부에 데이터 받을 수 있고
DataInputStream input = new DataInputStream(socket.getInputStream()); // 보조 스트림 사용 socket.getInputStream()
// 외부에 데이터를 보낼 수 있고
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
log("socket connected" + socket);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("메시지 입력: ");
String toSend = sc.nextLine();
output.writeUTF(toSend);
log("client -> server: " + toSend);
if (toSend.equalsIgnoreCase("exit")) {
break;
}
String received = input.readUTF();
log("server -> client: " + received);
}
// 자원 정리
log("연결 종료: " + socket);
input.close();
output.close();
socket.close();
}
}
/**
* V2 client 한명이 한 서버에 접속하면 잘 작동
* 하지만 여러명이 접속하면 서버가 한명에게만 응답을 해준다. socket과는 연결이 되어 있음
*/
package chap56.network.tcp.v2;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import static chap56.util.MyLogger.log;
public class ServerV2 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
log("Server started");
ServerSocket serverSocket = new ServerSocket(PORT); //12345 포트가 떠있음
log("Server socket created: " + PORT);
Socket socket = serverSocket.accept(); // 12345 포트로 들어오면 갖고 socket을 만들어줌
log("Client connected: " + socket);
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
while (true) {
// 클라이언트로부터 메시지 수신 (서버입장에서는 받는 입장)
String received = input.readUTF(); // 여기서 여러 스레드를 만들어서 돌려야 여러명 접속해더라도 각 스레드 마다 할당해서 작동
log("client -> server: " + received);
if (received.equalsIgnoreCase("exit")) {
break;
}
String toSend = received + " World!";
output.writeUTF(toSend);
log("server -> client: " + toSend);
}
// 자원 정리
log("연결 종료: " + socket);
input.close();
output.close();
socket.close();
serverSocket.close();
}
}
/**
* Task :chap56.network.tcp.v1.ServerV1.main()
* 20:52:32.54 [ main] Server started
* 20:52:32.54 [ main] Server socket created: 12345
* 20:52:40.97 [ main] Client connected: Socket[addr=/127.0.0.1,port=52307,localport=12345] port 52307에 연결 되어 있음
* 20:52:40.97 [ main] client -> server: Hello!
* 20:52:40.97 [ main] server -> client: Hello! Hello, Client!
* 20:52:40.97 [ main] 연결 종료: Socket[addr=/127.0.0.1,port=52307,localport=12345]
* 서로 통신 하기 위해서는 IP & PORT가 필요하다. 클라이언트가 접근을 할때 남는 Port 하나 할당
*/

TCP 3 way handshake가 완료되었기 때문에, 클라이언트와 서버의 TCP 연결은 이미 완료되고, 클라이언트의 소켓 객체도 정상 생성된다. 참고로 이 시점에 아직 서버의 소켓 객체(서버 소켓 아님)는 생성되지 않았다

50000번 클라이언트와 60000번 클라이언트 모두 서버와 연결이 완료되었고, 클라이언트의 소켓도 정상 생성

서버가 클라이언트와 데이터를 주고 받으려면 소켓을 획득
ServerSocket.accpet() 메서드 호출 -> backlog 큐의 정보를 기반으로 소켓 객체를 하나 생성
큐이므로 순서대로 데이터 꺼냄.

60000번 클라이언트도 이미 서버와 TCP 연결은 되어 있다.
OS 계층에서 TCP 3 way handshake가 발생하고, TCP 연결이 완료
이 문제를 프로토콜 계층에서 한번 보자

자바 애플리케이션은 소켓 객체의 스트림을 통해 서버와 데이터를 주고 받음. 데이터를 주고 받는 과정은
클라이언트가 보낸 메시지는 서버 애플리케이션에서 읽지 않았기 때문에 OS의 TCP 수신 버퍼에서 대기
소켓 객체 없이 서버 소켓만으로 TCP 연결은 완료.
하지만 연결 이후에 메세지 주고 받기 위해서는 소켓 객체 필요
accept()는 이미 연결된 TCP 연결 정보를 기반으로 서버 측에 소켓 생성, 그리고 소켓 객체가 있어야 스트림을 사용해서 메시지 주고 받을 수 있음

소켓을 연결하면 소켓의 스트림을 통해 OS TCP 수신 버퍼에 있는 메시지를 읽을 수 있고, 또 전송 가능
accept() 메서드는 backlog 큐에서 새로운 정보가 도달할 때 까지 블로킹 상태로 대기

package chap56.network.tcp.v3;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;
import static chap56.util.MyLogger.log;
public class ClientV3 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
log("Client started");
Socket socket = new Socket("localhost", PORT);
// 외부에 데이터 받을 수 있고
DataInputStream input = new DataInputStream(socket.getInputStream()); // 보조 스트림 사용 socket.getInputStream()
// 외부에 데이터를 보낼 수 있고
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
log("socket connected" + socket);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("메시지 입력: ");
String toSend = sc.nextLine();
output.writeUTF(toSend);
log("client -> server: " + toSend);
if (toSend.equalsIgnoreCase("exit")) {
break;
}
String received = input.readUTF();
log("server -> client: " + received);
}
// 자원 정리
log("연결 종료: " + socket);
input.close();
output.close();
socket.close();
}
}
/**
* V2 client 한명이 한 서버에 접속하면 잘 작동
* 하지만 여러명이 접속하면 서버가 한명에게만 응답을 해준다. socket과는 연결이 되어 있음
*/
package chap56.network.tcp.v3;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import static chap56.util.MyLogger.log;
public class SessionV3 implements Runnable{
private final Socket socket;
public SessionV3(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
//종료 해버리면, 서버가 연결 종료를 안하고 꺼버림
//GC는 자동으로 되는 것이 아님
while (true) {
//클라이언트로부터 문자 받기
String received = input.readUTF();
log("client -> server: " + received);
if (received.equalsIgnoreCase("exit")) {
break;
}
//클라이언트에게 문자 보내기
String toSend = received + " World!";
output.writeUTF(toSend);
log("server -> client: " + toSend);
}
//자원 정리
log("연결 종료: " + socket);
input.close();
output.close();
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
package chap56.network.tcp.v3;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import static chap56.util.MyLogger.log;
public class ServerV3 {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
log("Server started");
ServerSocket serverSocket = new ServerSocket(PORT);
log("Server socket created: " + PORT);
while (true) {
Socket socket = serverSocket.accept(); //블로킹
log("Client connected: " + socket);
//실제 하나랑 통신하는 것은 SessionV3 처리
SessionV3 session = new SessionV3(socket);
//별도 스레드로 처리
Thread thread = new Thread(session);
thread.start();
}
}
}
/**
* > Task :chap56.network.tcp.v3.ServerV3.main()
* 22:20:58.27 [ main] Server started
* 22:20:58.27 [ main] Server socket created: 12345
* 22:21:07.37 [ main] Client connected: Socket[addr=/127.0.0.1,port=53026,localport=12345]
* 22:21:17.67 [ main] Client connected: Socket[addr=/127.0.0.1,port=53030,localport=12345]
* 22:21:26.39 [ Thread-0] client -> server: fuck u
* 22:21:26.39 [ Thread-0] server -> client: fuck u World!
* 22:21:32.68 [ Thread-1] client -> server: fuck u
* 22:21:32.68 [ Thread-1] server -> client: fuck u World!
*
* 각각의 스레드가 따로따로 처리
*/
여기서의 문제는 서버에서 readUTF로 클라이언트가 메시지를 읽으려고 하면 EOFException 발생
소켓의 TCP 연결이 종료되었기 때문에 더는 읽을 수 없는 메시지, EOF(파일의 끝)가 여기서는 전송의 끝이
라는 뜻이다. 이러면 문제는 자원 정리가 하나도 안된 상태에서 종료가 된다는 뜻
자바 객체는 GC가 되지만 자바 외부의 자원은 자동으로 GC가 되는게 아니다. 따라서 꼭! 정리를 해주어야 한다.
package chap56.network.tcp.autocloseable;
public class ResourceV1 {
private String name;
public ResourceV1(String name) {
this.name = name;
}
public void call() {
System.out.println(name + " call");
}
public void callEx() throws CallException {
System.out.println(name + " call exception");
throw new CallException(name + " call exception");
}
//자원 정리
public void close() {
System.out.println(name + " close");
}
//정리중 예외 발생
public void closeEx() throws CloseException {
System.out.println(name + " close exception");
throw new CloseException(name + " close exception");
}
}
package chap56.network.tcp.autocloseable;
public class CloseException extends Exception{
public CloseException(String message) {
super(message);
}
}
package chap56.network.tcp.autocloseable;
public class CallException extends Exception{
public CallException(String message) {
super(message);
}
}
package chap56.network.tcp.autocloseable;
public class ResourceCloseMainV1 {
public static void main(String[] args) {
try {
logic();
} catch (CallException e) {
System.out.println("CallException: " + e.getMessage());
e.printStackTrace();
} catch (CloseException e) {
System.out.println("CloseException: " + e.getMessage());
e.printStackTrace();
}
}
private static void logic() throws CallException, CloseException {
ResourceV1 resource1 = new ResourceV1("resource1");
ResourceV1 resource2 = new ResourceV1("resource2");
resource1.call();
resource2.call();
System.out.println("자원 정리");
resource2.closeEx();
resource1.closeEx();
}
}
callEx 호출하면서 예외 발생, 예외 때문에 자원 정리 코드가 정상 호출 X
package chap56.network.tcp.autocloseable;
public class ResourceCloseMainV2 {
public static void main(String[] args) {
try {
logic();
} catch (CallException e) {
System.out.println("CallException: " + e.getMessage());
e.printStackTrace();
} catch (CloseException e) {
System.out.println("CloseException: " + e.getMessage());
e.printStackTrace();
}
}
private static void logic() throws CallException, CloseException {
ResourceV1 resource1 = null;
ResourceV1 resource2 = null;
try {
resource1 = new ResourceV1("resource1"); //만약 여기서 터지먄 ?
resource2 = new ResourceV1("resource2"); //resource2는 null이므로 close() 호출 안됨
resource1.call();
resource2.callEx(); // CallException 발생
} catch (Exception e) {
System.out.println("ex : " + e);
throw e;
} finally {
//resource2.closeEx();
//resource1.closeEx();// 이 코드 호출 안됨
if (resource2 != null) {
resource2.closeEx(); // CloseException 발생
}
// resource1이 close() 호출 안됨
if (resource1 != null) {
resource1.closeEx(); // 이 코드는 호출 X
}
}
}
}
/**
* > Task :chap56.network.tcp.autocloseable.ResourceCloseMainV2.main()
* resource1 call
* resource2 call exception
* ex : chap56.network.tcp.autocloseable.CallException: resource2 call exception
* resource2 close
* resource1 close
* CallException: resource2 call exception
*/
자원 정리중에 예외 발생
finally 블록은 항상 호출, 자원이 정리 될 것 같지만 이번에는 자원 정리 중 resource2.closeEx()를 호출하면서 예외가 발생 resource1.closeEx()는 호출 X
여기서 핵심은 CallException이지 CloseException이 아님.
package chap56.network.tcp.autocloseable;
public class ResourceCloseMainV3 {
public static void main(String[] args) {
try {
logic();
} catch (CallException e) {
System.out.println("CallException: " + e.getMessage());
e.printStackTrace();
} catch (CloseException e) {
System.out.println("CloseException: " + e.getMessage());
e.printStackTrace();
}
}
private static void logic() throws CallException, CloseException {
ResourceV1 resource1 = null;
ResourceV1 resource2 = null;
try {
resource1 = new ResourceV1("resource1"); //만약 여기서 터지먄 ?
resource2 = new ResourceV1("resource2"); //resource2는 null이므로 close() 호출 안됨
resource1.call();
resource2.callEx(); // CallException 발생
} catch (Exception e) {
System.out.println("ex : " + e);
throw e;
} finally {
//resource2.closeEx();
//resource1.closeEx();// 이 코드 호출 안됨
if (resource2 != null) {
try {
resource2.closeEx(); // CloseException 발생
} catch (CloseException e) {
// close에서 발생한 예외는 버린다. 필요하면 로깅정도.. ?
System.out.println("CloseException: " + e.getMessage());
e.printStackTrace();
//예외를 잡아서 처리하기 때문에 그 다음 아래 예외 처리 가능
}
}
// resource1이 close() 호출 안됨
if (resource1 != null) {
try {
resource1.closeEx(); // 이 코드는 호출 X
} catch (CloseException e) {
System.out.println("CloseException: " + e.getMessage());
e.printStackTrace();
}
}
}
}
}
/**
> Task :chap56.network.tcp.autocloseable.ResourceCloseMainV3.main()
resource1 call
resource2 call exception
ex : chap56.network.tcp.autocloseable.CallException: resource2 call exception
resource2 close exception
CloseException: resource2 close exception
resource1 close exception
CloseException: resource1 close exception
CallException: resource2 call exception
*/
finally 블럭에서 각각의 자원을 닫을때도 예외가 발생하면 예외 잡아서 처리
자원 정리 시점에 예외가 발생해도, 다음 자원을 닫을 수 있다.자원 정리 시점에 발생한 예외를 잡아서 처리했기 때문에, 자원 정리 시점에 발생한 부가 예외가 핵심 예외를 가리지 않는다.
package chap56.network.tcp.autocloseable;
public class ResourceCloseMainV4 {
public static void main(String[] args) {
try {
logic();
} catch (CallException e) {
System.out.println("CallException: " + e.getMessage());
e.printStackTrace();
} catch (CloseException e) {
System.out.println("CloseException: " + e.getMessage());
e.printStackTrace();
}
}
// try-with-resources 사용
// 자동으로 close 호출
private static void logic() throws CallException, CloseException {
try (ResourceV2 resource1 = new ResourceV2("resource1");
ResourceV2 resource2 = new ResourceV2("resource2")) {
resource1.call();
resource2.callEx(); // CallException 발생
} catch (CallException e) {
System.out.println("ex : " + e);
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* chap56.network.tcp.autocloseable.CallException: resource2 call exception
* Suppressed: chap56.network.tcp.autocloseable.CloseException: resource1 close exception
*
* closeException 발생 -> closeException을 Suppressed로 처리
* 핵심예외 안에 다른 예외를 담아두고, 핵심예외를 처리하면서 다른 예외를 처리할 수 있도록 함
*/
package chap56.network.tcp.autocloseable;
public class ResourceV2 implements AutoCloseable{
//AutoCloseable
//try-with-resources 구문을 사용하기 위해서는 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 + " call exception");
throw new CallException(name + " call exception");
}
//정리중 예외 발생
public void closeEx() throws CloseException {
System.out.println(name + " close exception");
throw new CloseException(name + " close exception");
}
@Override
public void close() throws Exception {
System.out.println(name + " close");
throw new CloseException(name + " close exception");
}
}
try with resource를 사용하면서의 장점
Try with resources 예외 처리와 부가 예외 포함
핵심 로직 예외와 자원을 정리하는 중에 발생하는 부가 예외가 모두 발생하면 어떻게 될까?