유닉스 소켓은 유닉스 기반 OS의 IPC(inter process communication) 메커니즘
의 일종이다.
유닉스 소켓은 같은 머신에서 작동하는 프로세스들이, local file descriptor
을 가지고 통신할 수 있도록 도와준다. file descriptor가 소켓 파일의 위치를 refer하는 기능을 수행한다. (유닉스는 모든 자원을 '파일'형태로 관리한다.)
통신이 커널 영역에서 이뤄지므로, 통신에 참여하는 프로세서는 file system에서 생성된 socket file에 접근할 수 있는 권한을 가져야 한다.
TCP/IP 소켓은 네트워크 스택을 거치는 반면, 유닉스 소켓은 커널을 통해 직접 소통하므로 상대적으로 빠르다.
EX) 같은 머신에서 작동하는 웹 서버와 데이터베이스 서버가 소통
TCP/IP socket은 TCP/IP protocol suite
의 근본적인 부분이다.
소켓
은 네트워크를 통해 데이터를 전송하고 수신하는 endpoint이다.
소켓은 IP 주소와 포트 번호로 구성된다. 이 둘을 통해, 특정 머신에서 작동하는 특정 프로세스를 식별할 수 있다.
소켓은 직접 초기화를 하는가, 연결을 허용하는가
에 따라 클라이언트, 서버 소켓 둘 다 될 수 있다.
TCP/IP 소켓은 네트워크를 통해 데이터를 전송할 수 있도록 하는 신뢰할 수 있고, 스트림 기반의 연결
을 제공한다.
TCP/IP 소켓은 OS의 커널에서 구현되고, 다양한 프로그래밍 언어로 활용할 수 있다.
자바 소켓
은 TCP/IP
소켓을 생성하고, 이를 통해 네트워크에서 프로그램들이 통신을 할 수 있도록 도와주는 Java API이다.
자바 소켓은 네트워크에서 TCP/IP protocol
에 따라 통신한다.
즉, 자바 소켓은 OS에서 제공하는 기능들을 사용할 수 있도록 도와주는 인터페이스 역할을 하는 TCP/IP
의 구현체이다.
이를 사용해서 다른 머신에서 동작하는 자바 프로그램끼리 통신을 할 수 있다.
자바 소켓은 SW 형태로 구현되고, TCP/IP를 지원하는 모든 네트워크 주소에 연결될 수 있다.
자바 소켓은 일반적으로 Clinet가 네트워크를 통해 Server에 연결되는 Client-Server
App을 만들때 사용된다.
package java.net;
class ServerSocket implements java.io.Closeable {
// 실제 소켓 작업을 수행하는 구현체를 필드로 가진다.
private SocketImpl impl;
// 소켓 팩토리 객체
private static SocketImplFactory factory = null;
public Socket accept() throws IOException {...}
}
ServerSocket
은 네트워크를 통해 요청이 오기를 기다린다. 요청에 기반한 operation을 수행한 뒤, 요청자에게 결과를 반환한다.
실제 ServerSocket
의 작업은 SocketImpl
인스턴스가 수행한다.
SocketImpl
도 사실은 추상 클래스이고, 내부적으로 SocketImpl
를 구현한 인스턴스들이상황에 맞춰 SocketImpl
객체에 할당된다.
App은 로컬 방화벽에 알맞은 소켓을 만들기 위해, socket구현체를 만드는 SocketFactory
를 변경할 수 있다.
class ServerSocket implements java.io.Closeable {
...
public Socket accept() throws IOException {
// ServerSocket이 close되어있으면 에러
if (isClosed())
throw new SocketException("Socket is closed");
// Socket이 특정 port에 bound되어있지 않으면 에러
if (!isBound())
throw new SocketException("Socket is not bound yet");
// Socket이 내부적으로 사용할 SocketImpl을 null로 set
Socket s = new Socket((SocketImpl) null);
// 1. SocketImplFactory보유 여부에 따라 socketImpl객체 생성후 초기화
// 2. 여러가지 정보 (request ip address 등) 초기화
// 3. Socket 상태 정보 초기화 (connected, created, bound)
implAccept(s); (throws SecurityException)
return s;
}
}
ServerSocket객체에 connection이 생성되는 것을 listen하고, accept한다.
accpet메서드는 connection이 생성될 때 까지 block한다.
securityManager가 요청을 검증한 뒤, 새로운 Socket 객체를 반환한다.
서버와 클라이언트의 Connection에서, 새로 생성된 Socket은 클라이언트의 소켓으로 사용된다.
public
class Socket implements java.io.Closeable {
...
public InputStream getInputStream() throws IOException {...}
public OutputStream getOutputStream() throws IOException {...}
}
Socket
클래스는 클라이언트측 소켓을 구현한다.
socket은 두 머신간의 소통의 양 끝단이다.
ServerSocket
과 마찬가지로
SocketImpl
객체가 수행한다.SocketFactory
를 통해 로컬 방화벽에 맞춘 Socket구현체를 변경할 수 있다.짧게 요약하자면
1. Socket
은 ClientSide의 Socket으로 사용된다.
2. ServerSocket
는 ServerSide의 Socket으로 사용된다.
// 요청을 처리하는 Class가 Runnable을 implement한 상황
public void run(Socket socket) {
sout(socket.getInetAddress()); // Client Socket에 저장된 Client IP address
sout(socket.getPort()); // Client Socket에 저장된 Client Port
// Client Socket에서 InputStream, OutputStream가져오기
try (InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream()) {
InputStreamReader inputStreamReader = new InputStreamReader(in);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
// InputStream에서 bufferedReader를 꺼내와서 Request message를 읽는다.
String line;
while (!(line = bufferedReader.readLine()).isEmpty()) {
System.out.println(line);
}
// OutputStream에서 DataOutputStream을 꺼내와서 Response message를 작성한다.
DataOutputStream dos = new DataOutputStream(out);
byte[] body = "Hello World".getBytes();
dos.writeByte("HTTP/1.1 200 OK");
dos.writeByte("Content-type:text/html");
dos.writeByte("Content-length:" + body.length);
...
dos.write(body, 0, body.length)
dos.flush();
} catch (IOException e) {
logger.error(e.getMessage());
}
}
Socket
은 getInputStream()
, outputStream()
메서드를 통해 socket의 InputStream, OutputStream을 불러온다.
정확히는 socket내부의 socketImpl의 InputStream, OutputStream을 불러오는 것이다.
public InputStream getInputStream() throws IOException {
if (isClosed()) // Socket이 닫혀있으면 에러
throw new SocketException("Socket is closed");
if (!isConnected()) // Socket이 연결이 끊겨있으면 에러
throw new SocketException("Socket is not connected");
// InpuStream이 닫혀있으면 에러 (ShutIn 필드 사용)
// 실제 InpuStream객체의 상태를 변경하지 않고, Socket 내부의 필드 값만 변경하는 듯
if (isInputShutdown())
throw new SocketException("Socket input is shutdown");
// 반환할 InputStream객체 초기화
InputStream is = null;
try {
is = AccessController.doPrivileged(
new PrivilegedExceptionAction<>() {
public InputStream run() throws IOException {
return impl.getInputStream();
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
return is;
}
public OutputStream getOutputStream() throws IOException {
if (isClosed())
throw new SocketException("Socket is closed");
if (!isConnected())
throw new SocketException("Socket is not connected");
if (isOutputShutdown())
throw new SocketException("Socket output is shutdown");
OutputStream os = null;
try {
os = AccessController.doPrivileged(
new PrivilegedExceptionAction<>() {
public OutputStream run() throws IOException {
return impl.getOutputStream();
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
return os;
}
getInputStream
메서드는 소켓의 InputStream
을 반환한다.
네트워크 SW가 Connection이 broke되었음을 감지하면 다음 내용이 InputStream에 적용된다.
InputStream을 close하면, 해당 InputStream을 반환한 Socket도 close된다.
getOutputStream
메서드는 소켓의 OutpuStream
을 반환한다.
InputStream
과 OutputStream
은 각각 데이터를 읽고, 쓰는 방법을 제공하는 Java IO API의 클래스이다.
InputStream
는 파일, 네트워크 연결 등으로부터 데이터를 읽을 때 사용한다.
OutputStream
은 파일, 네트워크 연결 등의 목적지로 데이터를 쓰는데 사용한다.
두 클래스 모두 추상 클래스이므로 직접 인스턴스를 생성할 수 없고, 각각을 구현한 서브 클래스를 사용해야한다.(EX: FileInputStream, ByteArrayOutputStream 등)
사용할 서브 클래스는 어떤 형태의 데이터 출처로부터 데이터를 불러올지
에 따라 달라진다.
FileInputStream: 파일 읽기
ByteArrayOutputStream: byte[]에 쓰기
...
대략적으로 어떻게 작동하는지 살펴보자
// InputStream
byte[] bytes = {'a', 'b', 'c', 'd'};
// 어디서 데이터를 읽어야할지 파라미터로 넘겨줘야 한다. (EX: FileInputStream은 파일 경로를 입력한다.)
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
// 스트림 하나 읽기
int read = byteArrayInputStream.read(); // 97 ('a'의 ascii code)
System.out.println(read);
// 스트림에서 데이터를 하나 읽었으므로, 스트림에 남은 데이터는 3개
// 모든 스트림 읽기
byte[] readBytes = byteArrayInputStream.readAllBytes();
for (byte readByte : readBytes) {
System.out.print(readByte + " "); // 98 99 100
}
int empty = byteArrayInputStream.read(); // 스트림이 비어있으면 -1
outputStream.write(...);
위 코드는 실제로 데이터를 write하지 않는다.
write
에 전달된 argument는 스트림 내부의 버퍼에 저장된다.
그렇다면 OutputStream은 언제 데이터를 실제로 write할까?
1. 버퍼가 꽉 찼을 때
BufferedOutputStream
은 생성 당시에 버퍼 사이즈를 지정할 수 있다.2. 스트림이 closed될 때
close
가 호출되면, 스트림은 자원을 정리하기 전에 우선 버퍼에 남아있는 데이터를 실제로 write한다.3. flush
메서드가 호출될 때
스트림은 입력받은 데이터를 즉각적으로 write하지 않고, 버퍼에 저장하여 과도한 IO비용을 줄일 수 있다는 장점이 있다.
그러나 network Socket을 통해 즉각적으로 데이터를 전송해야 하는 등의 상황에서는 한계로 작동할 수 있다.
이러한 문제를 해결하기 위해 flush
메서드가 등장했다.
flush는 다음의 특징을 갖는다.
flush
메서드를 통해 강제로 버퍼에 저장된 데이터를 write할 수 있다.flush
를 호출하면 버퍼는 empty가 된다.flush
를 호출해도 OutputStream은 closed되지 않는다.일부 구현체들은 자동으로 flush
를 호출하기도 한다.
byte[] bytes = {'a', 'b', 'c', 'd'};
// FileOutputStream은 파라미터로 전달받은 path에, 실제로 스트림에 buffer된 내용을 저장한다.
try (OutputStream outputStream = new FileOutputStream("test.txt")) {
for (byte aByte : bytes) {
outputStream.write(aByte);
}
outputStream.flush(); // flush가 호출되어도 스트림은 close되지 않는다. -> 스트림 재활용 가능
} catch (IOException e) {
}
일반적인 OutputStream은 byte (또는 int)만 write할 수 있다는 사용의 단점을 가진다.
DataOutputStream
는 app이 primitive Java data 타입들을 쉽게 write할 수 있도록 도와준다.