Socket의 getInputStream()과 getOutputStream() 메서드를 호출할 수 있다.
Server와 Client는 약속된 protocol에 따라 interact한다.
Server, Client close connection
서버는 다음 connection을 위해 Step2로 가고 다음 connection을 기다린다.
DaytimeServer -> DaytimeClient를 실행하여 localhost의 port 13번에서 서버가 실행되는 것을 확인한다.
위의 코드의 문제점은 무엇일까??
코드의 구조를 보면 connection request를 하나 받을 때까지 기다린다. 계속 기다리고 있다가 connection 들어오면 write를 해주고 다시 while문을 와서 다음 request를 기다린다.
만약에 client1번이 request를 보냈는데, 이 client가 굉장히 느린 client였다고 가정해보자. 그 다음 엄청나게 빠른 client2가 request를 보냈다고 가정해보자. 근데 아직도 client1이 아직도 request를 못받아서 client2는 계속 기다리고 있어야 한다.
--> IO blocking 대문에 기다려야하는 시간이 길어진다.
해결방법)
각각의 connection마다 새로운 process를 만든다 (옛날의 UNIX에서는 이렇게 동작함)
이렇게 하면 memory를 너무 많이 쓰기 때문에 각각의 connection마다 새로운 thread를 생성한다.
문제점 -> 앞의 방법의 문제점은??
만약에 request가 굉장히 많이 들어오면 thread가 엄청나게 생긴다
thread로 인한 overhead로 인해서 performance가 slow down된다
DoS 공격에도 취약해진다.
해결 방법)
Thread Pool을 사용한다
Thread Pool은 request들어올때마다 Thread를 생성하는 것이 아닌, Pool을 만들어서 이 thread를 이용해서 request를 해결한다.
Thread를 반납하고 다시 사용될 수 있도록 한다.
이상적인 thread pool size는 프로그램이 I/O bound인지 CPU bound인지에 따라 다르다.
Thread pool size가 너무 크면 memory가 높게 사용되기 때문에 본인의 상황에 맞게 사용하여야 한다.
그래서 보통은 다음과 같은 식에 따라 thread의 개수를 정한다.
isClosed and isBound
isClosed()
만약 ServerSocket이 닫혔으면 true를 리턴한다
ServerSocket object가 ServerSocket()으로 생성되고, port와 아직 연결되어 있지 않으면 닫힌 것으로 생각하지 않는다 -> false를 리턴
isBound()
ServerSocket이 한번이라도 port와 binding되었으면 True를 리턴한다.
문제는 isClosed()가 true라고 해서 열려있는 것은 아니다. 그럼 ServerSocket이 open되어있다는 사실을 어떻게 알 수 있을까?
JVM은 다른 프로그램을 실행시키는 것이 목적이다. 두 가지 기능이 있다고 말할 수 있는데,
자바 프로그램이 어느 기기나 운영체제 상에서도 실행될 수 있도록 한다
자바와 os 사이에서 중개자 역할을 수행하여 OS에 상관없이 재사용을 가능하게 해준다.
프로그램 메모리를 관리하고 최적화 한다
JVM은 코드를 실행하고, 해당 코드에 대해 런타임 환경을 제공하는 프로그램에 대한 사양이다.
JVM은 보통 어떤 기기상에서 실행되고 있는 프로세스, 특히 자바 앱에 대한 리소스를 대표하고 통제하는 서버를 지칭한다.
자바 애플리케이션을 클래스 로더를 통해 읽어들이고, 자바 API와 함께 실행하는 역할을 한다.
프로그램이 실행되면 JVM은 OS로부터 프로그램에 필요한 메모리를 할당받는다.
javac(자바 컴파일러)가 자바 소스코드를 읽고, 자바 바이트코드(.class)로 변환한다.
변경된 class 파일들을 클래스 로더를 통해 JVM 메모리 영역으로 로딩한다.
로딩된 class 파일들은 Execution engine을 통해 해석된다.
해석된 바이트 코드는 메모리 영역에 배치되어 수행된다. 이 때 JVM은 쓰레드 동기화나 가비지 컬렉션 같은 메모리 관리 작업을 수행한다.
[클래스 로더] : JVM은 런타임 시에 처음으로 클래스를 참조할 때 해당 클래스를 로드하고 메모리 영역에 배치시키는데 이 동적 로드를 담당하는 부분이 바로 클래스 로더이다.
Runtime Data Access
: JVM이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다.
PC 레지스터, JVM 스택, 네이티브 메서드 스택, 힙, 메서드 영역
PC 레지스터 : 스레드가 어떤 명령어로 실행되어야 할지 기록하는 부분 (JVM 명령의 주소를 가진다)
스택 : 지역변수, 매개변수, 메서드 정보, 임시 데이터 등을 저장
네이티브 메서드 스택 : 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역
힙 : 런타임에 동적으로 할당되는 데이터가 저장되는 영역. 객체나 배열 생성이 여기에 해당함. ( 힙에 할당된 데이터들이 가비지 컬렉터의 대상이 된다. JVM 성능에 영향을 미친다 )
메서드 영역 : JVM이 시작될 때 생성되고, JVM이 읽은 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드 및 메서드 코드, 정적 변수, 메서드의 바이트 코드 등을 보관한다.
Garbage Collection
C나 C++은 개발자가 직접 메모리를 관리했지만 자바에서는 JVM이 프로그램 메모리를 관리한다. JVM은 Garbage Collection이라는 프로세스를 통해 메모리를 관리하는데 참조되지 않은 객체들을 탐색하여 삭제하고 삭제된 객체의 메모리를 반환하고 힙 메모리를 재사용하는 과정을 거친다.
소켓을 들으면 일반적으로 전구의 소켓을 떠올릴 것이다. 그리고 컴퓨터에서 말하는 소켓은 전구의 소켓과 비슷하게 연결하는 통로라고 생각하면 된다. 사용자 즉, host가 2명이 있다고 생각해보자. Host가 서로 데이터를 주고 받는다고 생각해보자. 각각의 데이터는 단계별로 통과하여 데이터를 전달하게 되는데, 이 단계는 5가지가 있다. Application, Transport, Network, Link, Physical layer이다. 여기서 Application은 순수 software라고 생각하면 되고, Transport부터 Physical layer까지는 OS에 구현이 되어 있다. (Link layer와 Physical Layer는 NIC에도 있다.)
데이터 통신을 위해서는 먼저, 송신자의 Application에서 Socket이라는 문을 통하여 아래 layer들로 전달되고 수신자도 아래에서부터 데이터가 올라오고, Socket 문을 거쳐서 Application에 데이터가 도달하게 된다. 프로세스가 데이터를 보내거나 받기 위해서는 반드시 소켓을 열어 소켓에 데이터를 쓰거나 읽어야 한다.
Multiplexing / demultiplexing
Multiplexing과 demultiplexing은 transport layer에서 일어난다. 서로 반대되는 개념인데, multiplexing이 여러 데이터를 한 통로에 넣어서 섞어버리는 것이라면 demultiplexing은 한 통로에서 온 것을 여러 갈래 중 해당하는 곳으로 보내는 것을 의미한다. 우리가 인터넷 호스트에서 demultiplexing이라고 한다면 packet들이 도착했을때, packet들이 어떤 process로 가야되는지 보내는 것을 demultiplexing이라고 한다. Sender에서 multiplexing은 여러 개의 socket에서 오는 data를 handling하는 것을 의미한다. 즉, process마다 socket이 하나 있다고 생각하면 된다. physical layer에서 link, network layer까지는 같이 오다가 transport layer에서 어떤 socket으로 가는지 결정해준다.
어떻게 demultiplexing이 작동할까
Host는 IP layer(Network Layer)에서는 datagram을 전달받는다. 각각의 datagram은 source와 destination IP address를 가지고 있다. 각 datagram은 하나의 segment를 들고온다. 각 segment는 source와 destination의 port number를 가지고 있다. 그럼 host는 port number와 IP 주소를 이용해서 segment를 적절한 소켓에 데이터를 올려보내는 것이다.
TCP socket은 4가지 정보로 identify된다. Source의 IP주소, Soure의 port number, 목적지의 IP 주소, 목적지의 port number이다. 그럼 서버는 동시에 여러개의 TCP socket을 지원해야 된다. 각각의 socket은 각기 다른 socket으로 연결되는 것이다. socket은 port number에 binding되어야 하고, socket에 port number가 할당되어야만 socket의 기능을 할 수 있다. 위에서 볼 수 있듯이 16bit를 socket number로 사용할 수 있다. 이 중에서 0~1023은 잘 알려진 port로 임의로 사용할 수 없고(ex. 웹 서버:80포트) 임의로 사용할 수 있는 것은 49152~65535 번들이다.
하나의 프로세스는 같은 프로토콜, 같은 IP 주소, 같은 port number를 가지는 소켓을 여러 개 만들 수 있기 때문에, 하나의 프로세스는 하나의 port만으로도 여러 호스트에 있는 프로세스의 요청을 처리할 수 있다. (게임 서버에서 동시 접속자 수가 엄청나게 많을 수 있는 이유도 동일하다) 그래서 서버의 경우는 보통 하나의 포트만 할당받고, 하나의 포트로 여러 개의 소켓을 열게된다.
예시
위의 그림에서 모두 destination IP 주소는 B이고 port number는 80이다. 여기서 demultiplexing되어 다른 socket으로 들어간다.
Port number는 네트워크 상에서 통신하기 위해서 호스트 내부적으로 프로세스가 할당 받아야하는 고유한 숫자이다. 같은 Host 내에서 서로 다른 프로세스가 같은 Port number를 가질 수는 없다.
static이란 ‘고정된’이란 의미를 가지고 있다. Static 키워드를 사용해서 Static 변수와 Static 메소드를 만들 수 있는데 다른 말로 ‘정적 필드’와 ‘정적 메소드’ 라고도 하며, 합쳐서 정적 멤버라고 한다.
static field와 static method는 객체에 소속된 멤버가 아니라, 클래스에 고정된 멤버이다. 이 의미는 클래스 로더가 클래스를 로딩해서 메소드 메모리 영역에 적재할 때, 클래스 별로 관리된다. 따라서 클래스의 로딩이 끝나는 즉시 바로 사용할 수 있다.
Static member 선언
객체(인스턴스)로 생성할 것인지, static으로 생성할 것인지의 결정 기준은 공용으로 사용할 것이냐 아니냐로 결정하면 된다. 그냥 생성하면 java에서는 객체로 생성된다. static으로 생성하려면 static 키워드를 추가적으로 붙여서 공용으로 사용할 수 있다.
static field 사용 예시
먼저 Friend 클래스를 만들어 보았다. Friend에서는 static 변수인 numberOfFriends를 두었다. 이렇게 두면, Main에서 Friend의 static field를 어떻게 사용할 수 있는지 확인해보자.
package com.company;
public class Friend {
String name;
static int numberOfFriends;
Friend(String name){
this.name = name;
}
}
package com.company;
public class Main {
public static void main(String[] args) {
System.out.println(Friend.numberOfFriends);
}
}
Friend객체를 새로 생성하지 않고서 바로 numberOfFriends를 Friend 클래스에서 호출할 수 있다.
그럼 객체를 만들 때, 친구의 수를 한 명씩 늘리고, 이 변수를 공유할 수 있도록 해보자.
package com.company;
public class Friend {
String name;
static int numberOfFriends;
Friend(String name){
this.name = name;
numberOfFriends++;
}
}
////Main
package com.company;
public class Main {
public static void main(String[] args) {
Friend friend1 = new Friend("Jake");
Friend friend2 = new Friend("Heon");
System.out.println(Friend.numberOfFriends);
}
}
이렇게 하면 Friend의 numberOfFriend는 2가 출력되는 것을 확인할 수 있다.
우리가 별 생각없이 쓰던 Math.round()같은 함수도 static 함수이기때문에 사용할 수 있었던 것이다.
(Math 객체를 따로 만들지 않고 바로 Math.round()와 썼었다)
static 멤버의 특성
Static 키워드를 통해 생성된 정적멤버들은 Heap 영역이 아닌 Static 영역에 할당된다. Static 영역에 할당된 메모리는 모든 객체가 공유하여 하나의 멤버를 어디서든지 참조할 수 있는 장점을 가지지만 Garbage Collectior의 관리 영역 밖에 존재하기 때문에 Static영역에 있는 멤버들은 프로그램의 종료될때까지 메모리가 할당된 채로 존재하게 된다. 즉, 너무 남발하게 되면 문제가 생긴다.
클래스를 설계할 때, 멤버변수 중 모든 인스턴스에 공통적으로 사용해야하는 것에 static을 사용한다. : 객체를 생성하면 각 객체들은 서로 독립적이라서 서로 다른 값을 유지하기 때문에 공통적인 값을 필요로 하는 경우에 static을 붙인다.
static이 붙은 멤버변수는 인스턴스를 생성하지 않아도 사용할 수 있다. : static이 붙은 멤버 변수는 클래스가 메모리에 올라갈 때 이미 자동적으로 static 영역에 생성된다.
static이 붙은 메서드에서는 인스턴스 변수를 사용할 수 없다. : 인스턴스 변수는 인스턴스를 생성해야만 존재하기 때문에 static이 붙은 메서드를 호출할 때 인스턴스가 생성되어있음을 보장할 수 없기 때문에 static이 붙은 메서드에서 인스턴스 변수의 사용을 허용하지 않는다. (반대로 인스턴스 변수나 인스턴스 메서드에서는 static이 붙은 메서드를 사용할 수 있다.)
메서드 내에서 인스턴스 변수를 사용하지 않는다면, static을 붙이는 것을 고려한다. : 메서드의 작업내용중에서 인스턴스 변수를 필요로 한다면 static을 붙일 수 없다. 반대로 인스턴스 변수를 필요로 하지 않는다면 가능하면 static을 붙이는 것이 좋다. 메서드 호출시간도 짧아져서 효율이 높아진다. (static을 안붙인 메서드는 실행 시 호출되어야할 메서드를 찾는 과정이 추가적으로 필요하다.)
인스턴스 변수와 관계없거나, 클래스변수만을 사용하는 메서드들은 클래스메서드(static method)로 정의한다.
여기서 path의 하나하나를 children에 넣어준다. 예를 들어 WINNT는 처음 넣는 것이니까 curr_node.children에 없으므로 curr_node.children['WINNT']에는 Node('WINNT')가 들어가게 된다. 그리고 현재 노드를 WINNT인 노드로 바꾸어 준다.
위의 방법을 Trie의 원리로 구현한 것이다.
남은 것은 dfs를 이용해서 하나씩 출력만 하면 된다.
사전 순으로 출력을 해야 하므로 sorted(trie.head.children)을 해준다. 이는 제일 첫번째 level 0의 children들이다.
dfe에서는 node.key를 level만큼 띄워서 출력해주고, node의 children을 재귀문을 통하여 출력해준다.
import sys
input = sys.stdin.readline
class Node:
def __init__(self, key, data=None):
self.key = key
self.data = data
self.children = {}
class Trie:
def __init__(self):
self.head = Node(None)
def insert(self, path):
curr_node = self.head
for file in path:
if file not in curr_node.children:
curr_node.children[file] = Node(file)
curr_node = curr_node.children[file]
def dfs_node(node, depth):
print(' '*depth, node.key, sep='')
for child in sorted(node.children):
dfs_node(node.children[child], depth+1)
path_list = []
n = int(input())
trie = Trie()
for _ in range(n):
trie.insert(input().rstrip().split('\\'))
# print(trie.head.children['WINNT'].children)
for node in sorted(trie.head.children):
dfs_node(trie.head.children[node], 0)