Java 소켓(Socket)은 네트워크 통신을 위한 API입니다. 소켓(Socket)은 TCP/IP 기반의 연결 지향형 통신을 제공하며, 소켓 프로그래밍을 통해 클라이언트와 서버 간의 데이터 통신을 구현할 수 있습니다.
소켓(Socket) 관련 용어
`클라이언트와 서버`
클라이언트는 서버에 연결을 요청하고 데이터를 보내고 받는 쪽이며, 서버는 클라이언트의 요청을 수락하고 처리하는 쪽입니다.
`TCP/IP`
소켓은 TCP/IP 프로토콜을 기반으로 동작합니다. TCP는 신뢰성 있는 연결 지향형 통신을 제공합니다. IP는 데이터를 패킷으로 분할하여 전송하는 역할을 합니다. Java는 TCP 네트워킹을 위해 `java.net.Socket`과 `java.net.ServerSocket` 클래스를 제공합니다.
소켓 통신 흐름
`java.net.ServerSocket`: 연결 요청을 기다리면서 연결 수락을담당
`java.net.Socket`: 통신 담당
- 클라이언트에서 통신할 서버의 IP주소와 포트 번호를 안 상태에서 Socket을 생성하여 서버에 연결 요청을 합니다.
- 서버는 고정된 포트 번호에 바인딩하기 때문에, 서버에서 ServerSocket을 생성할 때 포트 번호를 하나 지정해줍니다. 위 그림에서는 8888로 포트 번호를 지정하여 SeverSocket을 생성한 이미지입니다.
- 클라이언트에서 Socket을 생성하여 연결을 요청하고, 서버에 있는 ServerSocket에서 요청을 수락하면(accept()) 서버는 통신용 Socket을 생성합니다.
- 클라이언트와 서버는 각각의 Socket을 이용해서 데이터를 주고 받습니다.
클라이언트 소켓(Socket) 생성
- 클라이언트는 ``java.net.Socket` 클래스를 사용하여 서버에 연결할 소켓을 생성합니다.
- 소켓 생성 시 서버의 IP 주소와 포트 번호를 지정해야 합니다.
- 클라이언트 소켓은 `InputStream`과 `OutputStream`을 통해 서버와 데이터를 주고받을 수 있습니다.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
public class ClientExample {
public static void main(String[] args) {
String serverIP = "127.0.0.1"; // 서버의 IP 주소
int serverPort = 8000; // 서버의 포트 번호
try {
// 클라이언트 소켓 생성
Socket socket = new Socket(serverIP, serverPort);
// 서버와의 입출력 스트림 확보
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
// 서버로 메시지 전송
String message = "Hello Server!";
outputStream.write(message.getBytes());
// 서버로부터 메시지 수신
byte[] buffer = new byte[1024];
int bytesRead = inputStream.read(buffer);
String receivedMessage = new String(buffer, 0, bytesRead);
System.out.println("Message From Server: " + receivedMessage);
outputStream.flush();
// 입출력 스트림및 소켓 닫기
inputStream.close();
outputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
서버 소켓(Socket) 생성
- 서버는 `java.net.ServerSocket` 클래스를 사용하여 클라이언트의 연결 요청을 수락할 소켓을 생성합니다.
- 서버 소켓은 특정 포트 번호에서 클라이언트의 연결 요청을 대기합니다.
- 클라이언트의 연결 요청이 있을 때마다 서버는 새로운 소켓을 생성하여 해당 클라이언트와 통신을 수행합니다.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerExample {
public static void main(String[] args) {
int serverPort = 8000; // 서버의 포트 번호
try {
// 서버 소켓 생성
ServerSocket serverSocket = new ServerSocket(serverPort);
while (true) {
// 클라이언트의 연결 요청 대기 및 수락
Socket clientSocket = serverSocket.accept();
// 클라이언트와의 입출력 스트림 확보
OutputStream outputStream = clientSocket.getOutputStream();
InputStream inputStream = clientSocket.getInputStream();
// 클라이언트로부터 메시지 수신
byte[] buffer = new byte[1024];
int bytesRead = inputStream.read(buffer);
String receivedMessage = new String(buffer, 0, bytesRead);
System.out.println("Message From Client: " + receivedMessage);
// 클라이언트로 메시지 전송
String message = "Hello Client!";
outputStream.write(message.getBytes());
outputStream.flush();
// 입출력 스트림및 소켓 닫기
inputStream.close();
outputStream.close();
clientSocket.close();
}
// 서버 소켓 닫기
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- `accept()`: 클라이언트의 연결 요청을 받기를 대기하다 요청이 오면 요청을 수락하고, 클라이언트와의 통신을 위한 `Socket` 객체를 반환합니다. `accept()` 메서드는 블로킹 메서드로, 클라이언트의 연결 요청이 들어올 때까지 실행을 멈추고 대기하다 연결 요청이 들어오면 해당 클라이언트와 통신을 위한 `Socket` 객체를 반환합니다.
ServerSocket 객체 생성시 바인딩 방법
1. 생성자에 바인딩할 포트 대입
ServerSocket serverSocket = new ServerSocket(port);
2. bind() 메서드를 이용하여 대입
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress(address, port));
`bind()` 메서드를 사용하여 IP 주소와 포트에 바인딩하고 서버 소켓을 생성할 수 있습니다. `bind()` 메서드를 사용할 때 IP 주소를 지정해줌으로, 서버는 지정된 주소의 지정된 포트로 들어오는 클라이언트의 연결 요청만을 수행합니다. 만약 IP주소를 지정해주지 않으면 서버는 모든 네트워크 인터페이스에 대해 포트를 바인딩합니다. 첫 번째 방법 역시 이와 마찬가지입니다.
다중 클라이언트 처리
위에서 보여준 서버의 코드는 단일 연결 처리만 할 수 있습니다. 다중 클라이언트에서 요청이 들어올 때 처리하기 위해서는 멀티스레드나 스레드 풀을 사용하여야 합니다. 이렇게 구현하면 각 클라이언트 연결은 별도의 스레드에서 처리되며, 서버는 클라이언트와의 통신을 병렬로 처리합니다.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class MultiClientServer {
public static void main(String[] args) {
int serverPort = 8000; // 서버의 포트 번호
try {
// 서버 소켓 생성
ServerSocket serverSocket = new ServerSocket(serverPort);
while (true) {
// 클라이언트의 연결 요청 수락
Socket clientSocket = serverSocket.accept();
// 클라이언트와의 통신을 처리하는 스레드 생성 및 시작
Thread clientThread = new ClientHandler(clientSocket);
clientThread.start();
}
// 서버 소켓 닫기
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 클라이언트와의 통신을 처리하는 스레드 클래스
private static class ClientHandler extends Thread {
private Socket clientSocket;
public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
try {
// 클라이언트와의 입출력 스트림 확보
InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();
// 클라이언트로부터 메시지 수신
byte[] buffer = new byte[1024];
int bytesRead = inputStream.read(buffer);
String receivedMessage = new String(buffer, 0, bytesRead);
System.out.println("Message From Client: " + receivedMessage);
// 클라이언트로 메시지 전송
String message = "Hello Client!!";
outputStream.write(message.getBytes());
// 입출력 스트림 및 소켓 닫기
inputStream.close();
outputStream.close();
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
소켓 예외 처리
소켓을 사용하다보면 네트워크 관련된 다양한 예외들이 있을 수 있습니다. 소켓과 관련된 주요 예외는 다음과 같습니다.
- `IOException`: 입출력 작업 중에 발생하는 일반적인 예외입니다. 소켓 연결이 끊기거나 통신 도중에 오류가 발생하는 경우에 이 예외가 발생할 수 있습니다.
- `SocketException`: 소켓 관련 예외입니다. 소켓 설정에 문제가 있거나 소켓이 닫혀 있는 경우에 이 예외가 발생할 수 있습니다.
- `ConnectException`: 서버에 연결할 수 없는 경우에 발생하는 예외입니다. 서버가 다운되었거나 네트워크 연결에 문제가 있는 경우에 이 예외가 발생할 수 있습니다.
- `UnknownHostException`: 주어진 호스트 이름이나 IP 주소를 해석할 수 없는 경우에 발생하는 예외입니다. DNS 조회에 실패하거나 잘못된 호스트 이름을 사용하는 경우에 이 예외가 발생할 수 있습니다.
- `SocketTimeoutException`: 소켓 작업(연결, 읽기, 쓰기 등)이 지정된 시간 내에 완료되지 않을 때 발생하는 예외입니다. 소켓 작업이 시간 초과될 경우에 이 예외가 발생할 수 있습니다.
최종 범용성 코드
- `try-with-resources`문을 사용하여 자원을 관리하도록 하였습니다.
- `PrintWriter`를 생성하고 `true`를 인자로 주어 자동으로 flush 되도록 하였습니다.
- `finally` 클라이언트 소켓을 닫도록 처리하였습니다.
- `BufferedReader`를 사용하여 문자열 단위로 데이터를 읽어옴으로 입출력 속도를 향상시켰습니다.
- 로직 처리는 따로 메서드로 분리하였습니다.
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class MultiClientServerExample {
public static void main(String[] args) {
int serverPort = 8000; // 서버의 포트 번호
try (ServerSocket serverSocket = new ServerSocket(serverPort)) {
while (true) {
Socket clientSocket = serverSocket.accept();
Thread clientThread = new Thread(new ClientHandler(clientSocket));
clientThread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
static class ClientHandler implements Runnable {
private Socket clientSocket;
public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
try (
InputStream inputStream = clientSocket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
OutputStream outputStream = clientSocket.getOutputStream();
PrintWriter writer = new PrintWriter(outputStream, true)
) {
String request = reader.readLine();
// 요청 처리 로직
String response = processRequest(request);
writer.println(response);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String processRequest(String request);
// 로직 처리
return "Hello Client!!";
}
}
}
'Java' 카테고리의 다른 글
Java 소켓을 사용하여 단체 채팅방 만들기 (0) | 2023.06.26 |
---|---|
Java try-wtih-resources로 자원 관리하기 (0) | 2023.06.23 |
Java 동기화와 비동기 처리 (0) | 2023.06.05 |
JAVA ThreadPoolExecutor을 이용한 스레드 풀(Thread Pool) (0) | 2023.05.25 |
Optional이란? (0) | 2022.12.05 |