User Tools

Site Tools


lecture:nrepl:sources

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
lecture:nrepl:sources [2013/03/05 09:05]
psk810 [클라이언트]
lecture:nrepl:sources [2019/02/04 14:26] (current)
Line 1: Line 1:
-====== 소스 분석 ====== +====== 프로젝트 구조 ​======
- +
-===== 프로젝트 구조 =====+
  
 <​code>​ <​code>​
Line 75: Line 73:
 </​code>​ </​code>​
  
-===== 프로젝트 설명 =====+====== 프로젝트 설명 ​======
  
 Leiningen 프로젝트가 아니라 Maven 프로젝트이다. 그래서 project.clj파일이 없다. ​ Leiningen 프로젝트가 아니라 Maven 프로젝트이다. 그래서 project.clj파일이 없다. ​
  
-==== 프로그램의 구동 시작 ====+===== 프로그램의 구동 시작 ​=====
  
 그래서 public static void main이 있는 main.java에서 시작된다. 그래서 public static void main이 있는 main.java에서 시작된다.
Line 104: Line 102:
   - clojure.tools.nrepl.cmdline에서 -main 을 호출한다.   - clojure.tools.nrepl.cmdline에서 -main 을 호출한다.
  
-===== Bencode =====+====== Bencode ​======
  
 Bencode.clj는 Bencode 인코딩/​디코딩 기능을 구현하고 있다. Bencode는 BitTorrent에서 사용하는 데이타 전송 및 저장을 위한 인코딩 방식으로 D. J. Bernstein(([[http://​cr.yp.to/​proto/​netstrings.txt|netstring]]))의 [[wp>​Netstring]]을 확장한 것이다. 바이너리 인코딩 방식에 비해 비효율적이지만,​ 단순하면서도 유연하고 컴퓨터 시스템마다 사용되는 엔디안이 달라도 그에 무관하게 동작한다는 특징이 있다. 스트링, 숫자, 리스트, 맵을 지원한다. Bencode.clj는 Bencode 인코딩/​디코딩 기능을 구현하고 있다. Bencode는 BitTorrent에서 사용하는 데이타 전송 및 저장을 위한 인코딩 방식으로 D. J. Bernstein(([[http://​cr.yp.to/​proto/​netstrings.txt|netstring]]))의 [[wp>​Netstring]]을 확장한 것이다. 바이너리 인코딩 방식에 비해 비효율적이지만,​ 단순하면서도 유연하고 컴퓨터 시스템마다 사용되는 엔디안이 달라도 그에 무관하게 동작한다는 특징이 있다. 스트링, 숫자, 리스트, 맵을 지원한다.
 +
 +===== netstring ​ 인코딩 =====
  
 우선 먼저 netstring은 다음과 같다. 우선 먼저 netstring은 다음과 같다.
Line 117: Line 117:
  
 처음 숫자는 데이타의 길이를 나타내고,​ 뒤이어 구분자로서 콜론(':'​)이 오며, 바로 뒤에 스트링 데이타가 온다. 마지막으로 ','​로 끝을 표시한다. 데이타의 길이가 먼저 나오기 때문에 어플리케이션은 미리 먼저 충분한 메모리를 확보할 수 있으며, 마지막 ','​는 에러 검증에 사용된다. 처음 숫자는 데이타의 길이를 나타내고,​ 뒤이어 구분자로서 콜론(':'​)이 오며, 바로 뒤에 스트링 데이타가 온다. 마지막으로 ','​로 끝을 표시한다. 데이타의 길이가 먼저 나오기 때문에 어플리케이션은 미리 먼저 충분한 메모리를 확보할 수 있으며, 마지막 ','​는 에러 검증에 사용된다.
 +
 +===== bencode 인코딩 =====
  
 bencode는 여기에 Integer, List, Dictionary 인코딩이 추가되었다. bencode는 여기에 Integer, List, Dictionary 인코딩이 추가되었다.
Line 130: Line 132:
   * bencoding은 JSON이나 YAML처럼 플랫폼에 독립적이지만 복잡하면서도 손쉽게 구조화되는 데이타 인코딩을 위한 목적이다.   * bencoding은 JSON이나 YAML처럼 플랫폼에 독립적이지만 복잡하면서도 손쉽게 구조화되는 데이타 인코딩을 위한 목적이다.
  
 +
 +===== bencode 소스 분석 =====
  
 이제 bencode.clj 클로져 소스 파일을 분석해 보자. 이제 bencode.clj 클로져 소스 파일을 분석해 보자.
 +
 +==== 상수 정의 =====
  
 소스에서는 우선 가장 먼저 becoding에 사용되는 구분자(delimiter)들을 상수로 정의하고 있다. 소스에서는 우선 가장 먼저 becoding에 사용되는 구분자(delimiter)들을 상수로 정의하고 있다.
Line 151: Line 157:
 (def colon 58) (def colon 58)
 </​code>​ </​code>​
 +
 +==== read-byte, read-bytes, read-long ====
  
 <code clojure> <code clojure>
Line 197: Line 205:
  
 string>​payload와 string>​payload 함수를 전방 선언한 것이다. :tag 메타데이타((클로져 1.0은 ^리더 매크로를 meta의 축약형으로 제공했지만,​ 클로져 1.1에서는 사용이 비승인되었다가,​ 클로져 1.2에서부터 #^가 사용이 비승인되면서 대신 사용하게 되었다. 하지만 ​ #^은 사용은 아직 가능하다.))를 "​[B"​와 java.lang.String으로 해서 리턴값의 타입을 지정한다. "​[B"​는 Java Byte Array []에 대한 Type Hint이다. ​ string>​payload와 string>​payload 함수를 전방 선언한 것이다. :tag 메타데이타((클로져 1.0은 ^리더 매크로를 meta의 축약형으로 제공했지만,​ 클로져 1.1에서는 사용이 비승인되었다가,​ 클로져 1.2에서부터 #^가 사용이 비승인되면서 대신 사용하게 되었다. 하지만 ​ #^은 사용은 아직 가능하다.))를 "​[B"​와 java.lang.String으로 해서 리턴값의 타입을 지정한다. "​[B"​는 Java Byte Array []에 대한 Type Hint이다. ​
 +
 +==== read-netstring ====
  
 <code clojure> <code clojure>
Line 212: Line 222:
     content))     content))
 </​code>​ </​code>​
 +
 +==== sting <-> bytes ====
  
 read-netstring*은 netstring의 처음 길이를 나타내는 숫자를 읽고, 다음 그 길이만큼의 데이타를 읽어 리턴한다. read-string 은 read-netstring*에서 리턴받은 것을 다시 리턴하는데,​ 만약 다음 데이타가 콤마가 아니면 예외를 던진다. read-netstring*은 netstring의 처음 길이를 나타내는 숫자를 읽고, 다음 그 길이만큼의 데이타를 읽어 리턴한다. read-string 은 read-netstring*에서 리턴받은 것을 다시 리턴하는데,​ 만약 다음 데이타가 콤마가 아니면 예외를 던진다.
Line 226: Line 238:
  
 string>​payload는 String을 입력받아 바이트 배열을 리턴하고,​ string<​payload는 바이트 배열을 입력받아 String을 리턴한다. string>​payload는 String을 입력받아 바이트 배열을 리턴하고,​ string<​payload는 바이트 배열을 입력받아 String을 리턴한다.
 +
 +==== write-netstring ====
  
 <code clojure> <code clojure>
Line 245: Line 259:
  
 write-netstring*과 write-netstring은 위의 read-netstring*과 read-netstring에 각각 대응된다. write-netstring*과 write-netstring은 위의 read-netstring*과 read-netstring에 각각 대응된다.
 +
 +==== read-token ====
  
 <code clojure> <code clojure>
Line 267: Line 283:
  
 함수 read-integer read-list read-map를 전방선언한다. 함수 read-integer read-list read-map를 전방선언한다.
 +
 +==== read-bencode ====
  
 <code clojure> <code clojure>
Line 318: Line 336:
  
 token-seq 함수는 '​e'​를 만날 때까지 bencode를 계속 읽어내는 Lazy-seq를 만들어 낸다. ->> 스레딩 문구의 시작 문구가 Input 스트림으로부터 bencode를 읽는 함수이다. 이것을 repeatedly로 무한 반복하는 Lazy-seq을 만든 indentify로 판단하는 take-while 문구를 통해 nil이 나올 때까지 읽어 들이게 된다. token-seq 함수는 '​e'​를 만날 때까지 bencode를 계속 읽어내는 Lazy-seq를 만들어 낸다. ->> 스레딩 문구의 시작 문구가 Input 스트림으로부터 bencode를 읽는 함수이다. 이것을 repeatedly로 무한 반복하는 Lazy-seq을 만든 indentify로 판단하는 take-while 문구를 통해 nil이 나올 때까지 읽어 들이게 된다.
 +
 +==== write-bencode ====
  
 <code clojure> <code clojure>
Line 362: Line 382:
 모든 write-bencoding 함수는 처음에는 접두어로 '​i',​ '​l',​ '​d'​를 쓰고, 접미사로 '​e'​를 쓴다. 모든 write-bencoding 함수는 처음에는 접두어로 '​i',​ '​l',​ '​d'​를 쓰고, 접미사로 '​e'​를 쓴다.
  
-===== 트랜스포트 =====+====== 트랜스포트 ​======
  
 Transport는 nREPL의 전송단을 추상화한 인터페이스이다. 외부에는 전송에 관한 내부 구현에는 관계없이 Transport를 이용하여 데이타의 송수신을 할 수 있다. ​ Transport는 nREPL의 전송단을 추상화한 인터페이스이다. 외부에는 전송에 관한 내부 구현에는 관계없이 Transport를 이용하여 데이타의 송수신을 할 수 있다. ​
 +
 +===== Transport 프로토콜 =====
  
 Transpoort는 다음과 같이 프로토콜로 정의되어 있다. Transpoort는 다음과 같이 프로토콜로 정의되어 있다.
Line 376: Line 398:
 recv는 다음 수신된 메세지를 읽어 리턴한다. 메세지가 없으면 대기한다. 대기중 timeout이 지나거나 전송 채널이 종료되면 nil을 리턴한다. recv는 다음 수신된 메세지를 읽어 리턴한다. 메세지가 없으면 대기한다. 대기중 timeout이 지나거나 전송 채널이 종료되면 nil을 리턴한다.
 send는 메시지를 전송 채널에 보내 송신한다. 이것은 Transport 자신을 다시 리턴한다. send는 메시지를 전송 채널에 보내 송신한다. 이것은 Transport 자신을 다시 리턴한다.
 +
 +===== FnTransport 타입 =====
  
 Transport 프로토콜의 구현은 FnTransport와 QueueTransport이 있다. QueueTransport는 파이프 트랜스포트로 사용되지만,​ FnTransport가 실제 bencode를 전송단에서 사용하고 있다. Transport 프로토콜의 구현은 FnTransport와 QueueTransport이 있다. QueueTransport는 파이프 트랜스포트로 사용되지만,​ FnTransport가 실제 bencode를 전송단에서 사용하고 있다.
Line 400: Line 424:
 </​code>​ </​code>​
  
 +===== fn-transport 함수 =====
  
 fn-transport는 실질적으로 Transport 구현을 만들어내는 함수이다. fn-transport는 실질적으로 Transport 구현을 만들어내는 함수이다.
Line 436: Line 461:
  
 __read 함수는 읽어들인 메시지가 예외인 경우 failure 아톰을 nil -> 예외로 변경하고 예외를 던진다. 이후 다른 스레드에서 읽기 시도를 하면 다시 예외를 던지도록 한다.__ __read 함수는 읽어들인 메시지가 예외인 경우 failure 아톰을 nil -> 예외로 변경하고 예외를 던진다. 이후 다른 스레드에서 읽기 시도를 하면 다시 예외를 던지도록 한다.__
 +
 +===== bencode 함수 =====
  
 bencode 함수는 전송단에 실제의 스트림을 연결하여 전송단을 완성한다. bencode 함수는 전송단에 실제의 스트림을 연결하여 전송단을 완성한다.
Line 474: Line 501:
  
  
-===== 클라이언트 =====+====== 클라이언트 ​======
  
 nREPL의 클라이언트는 ​ nrepl.clj 소스에서 구현되어 있다. 클라이언트는 서버와 마찮가지로 전송단인 소켓기반 Transport를 사용한다. ​ nREPL의 클라이언트는 ​ nrepl.clj 소스에서 구현되어 있다. 클라이언트는 서버와 마찮가지로 전송단인 소켓기반 Transport를 사용한다. ​
  
-==== 클라이언트의 구동 ====+===== 클라이언트의 구동 ​=====
  
 클라이언트는 다음과 같이 구동한다. 클라이언트는 다음과 같이 구동한다.
Line 499: Line 526:
   - ㄹㄹㄹ   - ㄹㄹㄹ
  
-==== connect 함수 분석 ====+===== connect 함수 분석 ​=====
  
 이제 소스를 분석해 보자. 우선 nrepl/​connect 함수이다. 이제 소스를 분석해 보자. 우선 nrepl/​connect 함수이다.
Line 517: Line 544:
 connect 함수의 trasport-fn 파라미터를 위해 transport 함수를 따로 만들어 주지 않는 한, 보통은 transport/​bencode를 사용하게 된다. transport-fn는 Transport 프로토콜을 구현한 Transport 객체를 리턴하는 함수이다. transport/​bencode는 Transport 프로토콜을 구현한 FnTransport 객체를 리턴한다. connect 함수는 결국은 전송단을 리턴하는 것이다. connect 함수의 trasport-fn 파라미터를 위해 transport 함수를 따로 만들어 주지 않는 한, 보통은 transport/​bencode를 사용하게 된다. transport-fn는 Transport 프로토콜을 구현한 Transport 객체를 리턴하는 함수이다. transport/​bencode는 Transport 프로토콜을 구현한 FnTransport 객체를 리턴한다. connect 함수는 결국은 전송단을 리턴하는 것이다.
  
-==== client 함수 분석 ====+===== client 함수 분석 ​=====
  
 다음으로 client 함수의 소스를 보자. 좀 길다. 다음으로 client 함수의 소스를 보자. 좀 길다.
Line 565: Line 592:
 이제 메시지를 받는 과정을 알아보자. ​ 이제 메시지를 받는 과정을 알아보자. ​
  
-=== reset 내부 함수 ===+==== reset 내부 함수 ​====
  
 이것은 client함수의 내부 함수 restart 함수에서 시작된다. 이것은 client함수의 내부 함수 restart 함수에서 시작된다.
Line 594: Line 621:
 이 함수는 transport 파라미터를 전달된 FnTransport 객체의 Transport 프로토콜의 recv함수를 호출하는 lazy 시퀀스를 repeatedly 함수로 만든 후, take-while로 유효한 범위까지의 시퀀스만 받아들인다. identity는 자기자신을 리턴하는 함수여서 recv함수가 nil을 리턴하면 거기까지가 유효한 범위가 된다. 결국 response-seq는 서버로부터 받은 메시지의 시퀀스를 리턴하게 된다. 이것은 lazy 시퀀스가 아니다. 이 함수는 transport 파라미터를 전달된 FnTransport 객체의 Transport 프로토콜의 recv함수를 호출하는 lazy 시퀀스를 repeatedly 함수로 만든 후, take-while로 유효한 범위까지의 시퀀스만 받아들인다. identity는 자기자신을 리턴하는 함수여서 recv함수가 nil을 리턴하면 거기까지가 유효한 범위가 된다. 결국 response-seq는 서버로부터 받은 메시지의 시퀀스를 리턴하게 된다. 이것은 lazy 시퀀스가 아니다.
  
-=== tracking-seq 내부 함수 ===+==== tracking-seq 내부 함수 ​====
  
 다음은 reset 함수를 지원하는 함수 tracking-seq이다. 이 함수는 client 함수의 내부 함수이다. 다음은 reset 함수를 지원하는 함수 tracking-seq이다. 이 함수는 client 함수의 내부 함수이다.
Line 630: Line 657:
 하지만 이 함수는 부수적으로 update 함수를 통해 다른 일을 하는 것이 진짜 목적이다. ​ 하지만 이 함수는 부수적으로 update 함수를 통해 다른 일을 하는 것이 진짜 목적이다. ​
  
-=== update 내부 함수 ===+==== update 내부 함수 ​====
  
 이제 client 함수의 내부 함수인 update 함수를 보자. 이제 client 함수의 내부 함수인 update 함수를 보자.
Line 648: Line 675:
 update 함수는 atom은 latest-head를 수정하는 것인데, 이를 통해 responses 시퀀스의 각 원소를 순서대로 접근하게 될 때 동기적-비조율적 접근이 가능하게 된다. update 함수는 atom은 latest-head를 수정하는 것인데, 이를 통해 responses 시퀀스의 각 원소를 순서대로 접근하게 될 때 동기적-비조율적 접근이 가능하게 된다.
  
-=== client 함수가 하는 일 ===+==== client 함수가 하는 일 ====
 **__결국 client 함수가 리턴하는 함수가 리턴하는 것은 서버로부터 받은 메시지에 대한 동기적-비조율적인 원소 접근(접근시의 시각 포함해서)이 가능한 lazy 시퀀스이다.__** **__결국 client 함수가 리턴하는 함수가 리턴하는 것은 서버로부터 받은 메시지에 대한 동기적-비조율적인 원소 접근(접근시의 시각 포함해서)이 가능한 lazy 시퀀스이다.__**
  
 +===== message 함수 분석 =====
 +
 +다음은 message 함수 소스이다.
 +
 +<code clojure>
 +(defn message
 +  [client {:keys [id] :as msg :or {id (uuid)}}] ​                 ; 1.
 +  (let [f (delimited-transport-seq client #​{"​done"​} {:id id})]   ; 3. 
 +    (f (assoc msg :id id)))) ​                                    ; 2.
 +</​code>​
 +
 +  - 첫 인자 client는 repl/​client함수가 리턴한 함수가 되고, 나머지 인자는 맵으로 받아서 맵 인수분해를 하고 있다. 맵 인수분해에서는 id만 관심을 갖는데, :id가 없는 경우 uuid를 호출하고 있다. uuid는 misc.clj에 있는 함수이다.
 +  - 두번재 인자로 받은 msg에 uuid를 추가한 후 전송한다.
 +  - delimited-transport-seq 함수는 서버로부터의 응답 메시지중 필요한 것만 뽑아내는 기능을 client 함수에 추가한다.
 +
 +message 함수의 첫 인자는 repl/client 함수에서 만든 함수로서 이미 설명한 바 있다. 이 함수는 서버로부터 받은 메시지에 대해 동기적-비조율적 접근 가능한 lazy 시퀀스를 만들어 낸다. message 함수는 이 시퀀스를 한 번 더 필터링하기 위해 delimited-transport-seq 함수를 사용한다. ​
 +
 +다음은 delimited-transport-seq 함수의 소스이다.
 +
 +<code clojure>
 +(defn- delimited-transport-seq
 +  [client termination-statuses delimited-slots]
 +  (with-meta
 +    (comp (partial take-until (comp #(seq (clojure.set/​intersection ​           ; 4.
 +                                               % termination-statuses)) ​  
 +                                    set
 +                                    :​status)) ​
 +          (let [keys (keys delimited-slots)]
 +            (partial filter #(= delimited-slots (select-keys % keys)))) ​       ; 3.
 +          client ​                                                              ; 2.
 +          #(merge % delimited-slots)) ​                                         ; 1.
 +    (-> (meta client)
 +      (update-in [::​termination-statuses] (fnil into #{}) termination-statuses)
 +      (update-in [::​taking-until] merge delimited-slots))))
 +</​code>​
 +
 +delimited-transport-seq 함수도 comp를 사용하여 함수를 만들어 리턴하는 고계 함수이다. comp를 사용하고 있기 때문에 뒤에서부터 살펴보자.
 +
 +  - comp가 만드는 함수에 전달되는 인자는 메시지이다. 메시지는 맵 형태인데,​ delimited-slots와 병합하고 있다.
 +  - 위의 결과, 즉 delimited-slots와 병합한 메시지를 인자로 해서 client 함수 호출.
 +  - client 함수 호출 결과인 lazy 시퀀스들 중에서 delimited-slots과 같은 메시지만 필터링한다. select-keys는 첫 인자로 주어진 맵에서 두번째 인자로 주어진 키만으로 된 맵을 리턴한다. delimited-slots이 {:id uuid}였기 때문에, 결국 이것은 클라이언트가 보낸 메시지의 uuid와 같은 uuid를 지닌 서버 응답 메시지만 골라낸다.
 +  - 위의 결과에서 :status가 termination-statuses인 요소를 만날 때까지 가져온다. take-until 함수는 take-while과 같은데, 판단함수를 만족할 때까지의 시퀀스를 리턴한다. 여기서는 "​done"​을 만날때까지가 되겠다.
 +
 +message 함수에서 한가지 이상한 것은 with-meta를 사용하고 있다는 점이다. comp로 만들어진 함수에 client의 메타데이타에 termination-statuses와 delimited-slots를 갱신한 메타데이타를 설정하고 있다. 하지만 이 메타데이타가 사용되는 곳을 찾지 못했다.
 +
 +
 +
 +===== response-values 함수 분석 =====
 +
 +다음은 response-values 함수이다.
 +
 +<code clojure>
 +(defn response-values
 +  [responses]
 +  (->> responses
 +    (map read-response-value)
 +    combine-responses
 +    :value))
 +</​code>​
 +
 +<code clojure>
 +(defn read-response-value
 +  [{:keys [value] :as msg}]
 +  (if-not (string? value)
 +    msg
 +    (try
 +      (assoc msg :value (read-string value))
 +      (catch Exception e
 +        (throw (IllegalStateException. (str "Could not read response value: " value) e))))))
 +</​code>​
 +
 +<code clojure>
 +(defn combine-responses
 +  [responses]
 +  (reduce
 +    (fn [m [k v]]
 +      (case k
 +        (:id :ns) (assoc m k v)
 +        :value (update-in m [k] (fnil conj []) v)
 +        :status (update-in m [k] (fnil into #{}) v)
 +        :session (update-in m [k] (fnil conj #{}) v)
 +        (if (string? v)
 +          (update-in m [k] #(str % v))
 +          (assoc m k v))))            ​
 +    {} (apply concat responses)))
 +</​code>​
 +
 +====== 서버 ======
 +
 +nREPL의 서버는 server.clj 에서 구현되었다. ​
 +
 +===== 서버의 구동 =====
 +
 +서버를 구동시키기 위해서는 start-server 함수를 호출해야 한다.
 +
 +<code clojure>
 +(defn start-server
 +  [& {:keys [port bind transport-fn handler ack-port greeting-fn] :or {port 0}}]     ; 1.
 +  (let [bind-addr (if bind (InetSocketAddress. bind port) (InetSocketAddress. port)) ; 2.
 +        ss (ServerSocket. port 0 (.getAddress bind-addr))
 +        server (assoc ​                                                               ; 3.
 +                 ​(Server. ss
 +                          (.getLocalPort ss)
 +                          (atom #{})
 +                          (or transport-fn t/bencode)
 +                          greeting-fn
 +                          (or handler (default-handler)))
 +                 ;; TODO here for backward compat with 0.2.x; drop eventually
 +                 :ss ss)]
 +    (future (accept-connection server)) ​                                             ; 4.
 +    (when ack-port
 +      (ack/​send-ack (:port server) ack-port)) ​                                       ; 5.
 +    server))
 +</​code>​
 +
 +  - 이 함수는 인자들을 맵으로 받아 맵 인수분해를 하고 있다. 다음은 함수의 파라미터들에 대한 설명이다.
 +    * port : 서버 리스닝 포트. 기본으로 0으로 설정되어 있어, 시스템에서 빈 포트로 자동 할당.
 +    * bind : 서버 리스닝 IP 주소. 넷트웍 인터페이스가 여러 개일 경우 지정. 기본은 0.0.0.0.
 +    * handler : 클라이언트 메시지를 처리하기 위한 핸들러. 기본은 ​ defualt-handler.
 +    * ack-port : 어떤 포트값이 주어진다면,​ 새로 구동되는 서버의 포트를 알려주기 위한 다른 서버 포트. 클로져 도구 구현시에만 유용.
 +  - InetSocketAddress 자바 클래스를 사용하여 bind-address를 만들고 있다. bind가 참일 경우는 해당 ip주소를 사용하고 아닐경우에는 localhost가 된다.
 +  - ServerSocket 자바 클래스를 서버 소켓을 생성하여 ss로 받는다.
 +  - Server는 server.clj에서 정의된 defrecord이다. Server 레코드 생성후 :ss 슬롯 추가.
 +  - future 함수로 accept-client 함수를 스레드 구동.
 +  - ack-port 서버에 신규 서버의 포트를 알림.
 +
 +start-server는 ServerSocket 자바 클래스를 활용하여 리스닝 포트를 열고 Server 레코드를 만든 후, accept-connection 함수에 나머지 기능 이관하고 있다.
 +
 +===== 연결 접속 ​ =====
 +
 +<code clojure>
 +(defn- accept-connection
 +  [{:keys [^ServerSocket server-socket open-transports transport greeting handler] ​  ; 1.
 +    :as server}]
 +  (when-not (.isClosed server-socket)
 +    (let [sock (.accept server-socket)] ​                                             ; 2.
 +      (future (let [transport (transport sock)] ​                                     ; 3.
 +                (try
 +                  (swap! open-transports conj transport) ​                            ; 4.
 +                  (when greeting (greeting transport)) ​                              ; 5.
 +                  (handle handler transport) ​                                        ; 6.
 +                  (finally
 +                    (swap! open-transports disj transport) ​                          ; 7
 +                    (.close transport)))))
 +      (future (accept-connection server))))) ​                                        ; 8
 +</​code>​
 +
 +  - 함수는 Server 레코드를 인자로 받는다. 이 인자는 맵 인수분해되어 다음의 파라미터가 설정된다.
 +    * server-socket : start-server에서 생성한 ServerSocket 개체.
 +    * open-transports : 지금까지 전달된 transport의 누적. 처음엔 #{}.
 +    * transport : 메시지 송수신을 위한 전송단. start-server에 전달된 것이 없으면 기본으로 transport/​bencode를 받음.
 +    * greeting : 처음 접속시 인삿말 전송하는 함수.
 +    * handler : 메시지를 처리하는 핸들러. start-server에 전달된 것이 없으면 기본으로 default-handler를 받은.
 +  - 서버 소켓으로부터 클라이언트의 접속을 받는 코드이다. 접속이 없으면 무한 대기 상태. 서버측 접속 소켓은 sock으로 받음.
 +  - 새로운 스레드 생성. 서버측 접속 소켓으로 transport/​bencode 함수를 호출하여 FnTransprt 개체 생성하여 transport 지역 변수에 지정.
 +  - open-transports 에 새로 생긴 transport 추가.
 +  - 환영 인사. 현재는 greering 함수는 nil.
 +  - hadler로 transport의 메시지를 처리하기 위해 handle 함수 호출.
 +  - finally 처리 : open-transports에서 현재의 transport를 제거하고 종료 처리.
 +  - 새로운 스레드 생성. accept-connection 함수 호출하여 다음 클라이언트 접속을 대기한다.
 +
 +accept-connection 함수는 접속이 들어오면 스레드를 생성하여 접속을 처리하고,​ 바로 또 스레드를 생성하여 자기 자신을 호출하고 끝난다. 자기자신을 호출하는 새로운 스레드에 의해 무한 루프를 돌게 된다. ​
 +
 +접속이 들어오면 환영 인사를 하고 본격적으로 메시지 처리를 하기 위해 handle 함수를 호출하는데,​ 이 함수는 메시지가 없을 때까지 계속 Transport에서 메시지를 읽어들여 메시지를 처리한다.
 +
 +접속이 들어올 때마다 해당 접속의 Transport를 open-transports 에 추가한다. 이렇게 하는 이유는 Transport가 쓰레기 수집 처리되지 않게 하기 위해서 인 듯... 그래서 finally 처리시 open-transports 에서 해당 Transport 제거한다.
 +
 +===== 메시지 수신 및 처리 =====
 +
 +다음은 handle 함수이다.
 +
 +<code clojure>
 +(defn handle
 +  [handler transport]
 +  (when-let [msg (t/recv transport)] ​                                       ; 1.
 +    (repl/myprn msg "​server print"​)
 +    (future (handle* msg handler transport)) ​                               ; 2.
 +    (recur handler transport))) ​                                            
 +
 +(defn handle*
 +  [msg handler transport]
 +  (try
 +    (handler (assoc msg :transport transport)) ​                             ; 3.
 +    (catch Throwable t
 +      (log t "​Unhandled REPL handler exception processing message"​ msg))))
 +</​code>​
 +
 +  - Transport 개체의 recv 함수 호출하여 메시지를 받는다. 메시지가 없으면, 즉 nil이면, 종료.
 +  - 새로운 스레드 생성하여 받은 메시지를 처리. 이때 handle* 함수 호출.
 +  - 받은 메시지에 전송단 추가하여 handler 호출. 본격적인 메시지 처리 수행.
 +
 +handle 함수는 메시지를 받을 때마다 새로운 스레드를 생성하여 처리하고 있다. //좀 비효율적이지만 이렇게 하면 해당 메시지 처리시 예외가 발생해도,​ 다음 메시지 처리에 전혀 지장을 주지 않는다는 장점이 있을 듯...//
 +
 +handle* 함수의 handler 파라미터의 인자는 default-handler인데,​ 이것은 default-middlewares로 스택을 구성하고 unknown-op 함수를 기본 핸들러로 하여 핸들러를 구성하는 함수를 리턴한다.
 +
 +<code clojure>
 +(defn unknown-op
 +  [{:keys [op transport] :as msg}]
 +  (t/send transport (response-for msg :status #{:error :unknown-op :done} :op op)))
 +
 +(def default-middlewares
 +  [#'​clojure.tools.nrepl.middleware/​wrap-describe
 +   #'​clojure.tools.nrepl.middleware.interruptible-eval/​interruptible-eval
 +   #'​clojure.tools.nrepl.middleware.load-file/​wrap-load-file
 +   #'​clojure.tools.nrepl.middleware.session/​add-stdin
 +   #'​clojure.tools.nrepl.middleware.session/​session])
 +
 +(defn default-handler
 +  [& additional-middlewares]
 +  (let [stack (middleware/​linearize-middleware-stack (concat default-middlewares ​         ; 1. 
 +                                                             ​additional-middlewares))]
 +    ((apply comp (reverse stack)) unknown-op))) ​                                          ; 2.
 +</​code>​
 +
 +  - default-middlewares 와 파라미터 additional-middlewares로 미들웨어 스택을 구성한다.
 +  - unknown-op를 기본 핸들러로 하여 미들웨어 스택을 적용하는 함수 리턴.
 +
 +미들웨어는 핸들러 함수를 인자로 받아서 새로운 기능을 추가한 신규 핸들러 함수를 만들어서 리턴한다. 이 신규 핸들러 함수는 다시 다른 미들웨어가 받아서 또 새로운 기능을 추가한 신규 핸들러를 만들어 낼 수 있다. 이것을 다음과 같이 도식으로 설명할 수 있다.
 +
 +  a-handler -> [b-middleware] -> ab-handler -> [c-middleware] -> abc-handler
 +
 +
 +a 기능을 하는 핸들러 a-handler가 b 기능을 갖는 미들웨어에 인자로 들어가면 a와 b 기능을 하는 ab-handler가 리턴된다. 다시 ab-handler가 c 기능을 하는 미들웨어 c-middleware에 인자로 들어가면 a와 b와 c 기능을 하는 abc-handler가 리턴되는 식이다.
 +
 +===== 핸들러와 미들웨어 =====
 +
 +핸들러는 메시지 인자 하나만을 받는 함수이고,​ 미들웨어는 핸들러 인자 하나만을 받는 인자이다. 그리고 메시지는 맵이다. 따라서 미들웨어는 다음과 같은 형태를 갖는다.
 +
 +<code clojure>
 +(defn b-middleware
 + ​[handler-fn]
 + (fn [{:keys [key1 key2 ...] :as msg}]
 +    ...
 +    (let [msg (assoc msg :key3 value3)]
 +      (if ...
 +        (transport/​send transport (response-for msg ....))
 +        (h msg)))))
 +</​code>​
 +
 +미들웨어 함수는 기존 핸들러 함수를 인자로 받아 신규 핸들러 함수를 만들어 내므로 fn 함수를 사용하고 있다. 이 함수는 신규 핸들어 함수이므로 msg를 인자로 받는데, msg가 맵이라서 기본적으로 맵 인수분해를 하여, 해당 미들웨어가 관심을 갖는 키워드를 추출한다. 그리고 신규 핸들러 함수가 받는 인자인 msg에 새로운 키워드를 추가하거나 아니면 값을 변경하거나 해서 msg에 새로운 맵으로 만든후 기존 핸들러 함수에 인자로 해서 호출한다.
 +
 +최초의 핸들러 함수를 기본 핸들러 함수라고 한다. nREPL에서는 기본 핸들러 함수가 unknown-op 이다. 이 함수는 클라이언트가 보낸 메시지가 알 수 없는 코드가 있을 경우에 동작되는 함수이다.
 +
 +response-for 함수는 클라이언트로부터 받은 msg의 :id와 :​session으로 response-data를 구성해서 클라이언트에 응답하는 misc.clj에 정의된 함수이다.
 +
 +미들웨어는 자기 스스로 기존 핸들러 함수를 무시하고 response-for 함수로 클라이언트에 응답을 보내거나 아니면 단지 메시지를 변경한 다음 기존 핸들어 함수를 호출할 수도 있다. ​
 +
 +nREPL에서는 다음 5 개의 미들웨어가 default-middlewares로 지정되어 사용되고 있다.
 +
 +  * wrap-describe : "​describe"​ op 처리.
 +  * interruptible-eval : "​eval",​ "​interrupt"​ op 처리.
 +  * wrap-load-file : "​load-file"​ op 처리. ​
 +  * add-stdin : "​eval",​ "​stdin"​ op 처리.
 +  * session : "​ls-sessions",​ "​close",​ "​clone"​ op 처리.
 +  * pr-values : 메시지의 value 값을 프린트.
 +
 +linearize-middleware-stack 함수는 middleware.clj에서 정의된 함수로 미들웨어의 의존성에 따라 미들웨어들을 재배열시킨다.
 +
 +======= 미들웨어 =======
 +
 +nREPL 서버는 작은 단위의 기능들을 조합해서 서버의 기능을 구성해 낸다. 미들웨어는 이러한 작은 단위의 기능을 구현한다. 하지만 미들웨어가 서로 연결되는 순서가 중요해진다. 예를 들어 세션 미들웨어는 사용자의 세션을 메시지에 추가하는데,​ 이런 후에야 평가 미들웨어가 세션 데이타를 참조할 수 있다. 미들웨어의 순서를 결정하기 위해 각 미들웨어에 미들웨어 설명자를 메타데이타로 정의한다.
 +
 +===== 미들웨어 설명자 =====
 +
 +===== 미들웨어 재배치 =====
 +
 +==== linearize-middleware-stack ====
 +
 +linearize-middleware-stack 함수는 미들웨어 리스트를 입력받아서,​ 각 미들웨어 설명자에 기술된 미들웨어 의존성 정보에 맞추어서 미들웨어의 순서를 정한다.
 +
 +<code clojure>
 +(defn linearize-middleware-stack
 +  [middlewares]
 +  (->> middlewares
 +    extend-deps
 +    (sort-by (comp count (partial apply concat) (juxt :expects :​requires)))
 +    reverse
 +    (reduce #​(conj-sorted % comparator %2) [])
 +    (map :​implemented-by)))
 +</​code>​
 +
 +이 함수는 크게 extend-deps 함수에 의존하고 있다.
 +
 +====  extend-deps ====
 +
 +이 함수는 각 미들웨어 설명자에 기술된 의존성을 확장한다.
 +
 +<code clojure>
 +(defn- extend-deps
 +  [middlewares]
 +  (let [descriptor #(-> % meta ::​descriptor)
 +        middlewares (concat middlewares
 +                            (->> (map descriptor middlewares)
 +                              (mapcat (juxt :expects :requires))
 +                              (mapcat identity)
 +                              (filter var?)))]
 +    (doseq [m (remove descriptor middlewares)]
 +      (binding [*out* *err*]
 +        (printf "​[WARNING] No nREPL middleware descriptor in metadata of %s, see clojure.tools.middleware/​set-descriptor!"​ m)))
 +    (let [middlewares (set (for [m middlewares]
 +                             ​(->​ (descriptor m)
 +                               ; only conj'​ing m here to support direct reference to
 +                               ; middleware dependencies in :expects and :requires,
 +                               ; e.g. interruptable-eval'​s dep on
 +                               ; clojure.tools.nrepl.middleware.pr-values/​pr-values
 +                               ​(update-in [:handles] (comp set #(conj % m) keys))
 +                               ​(assoc :​implemented-by m))))]
 +      (set (for [m middlewares]
 +             ​(reduce
 +               #​(update-in % [%2] into (dependencies middlewares % %2))
 +               m #{:expects :​requires}))))))
 +</​code>​
 +
 +==== dependencies ====
 +
 +<code clojure>
 +(defn- dependencies
 +  [set start dir]
 +  (let [ops (start dir)
 +        deps (set/select
 +               (comp seq (partial set/​intersection ops) :handles)
 +               set)]
 +    (when (deps start)
 +      (throw (IllegalArgumentException.
 +               ​(format "​Middleware %s depends upon itself via %s"
 +                       ​(:​implemented-by start)
 +                       ​dir))))
 +    (concat ops
 +            (mapcat #​(dependencies set % dir) deps))))
 +</​code>​
 +
 +==== comparator ====
 +
 +<code clojure>
 +(defn- comparator
 +  [{a-requires :requires a-expects :expects a-handles :handles}
 +   ​{b-requires :requires b-expects :expects b-handles :handles}]
 +  (or (->> (into {} [[[a-requires b-handles] -1]
 +                     ​[[a-expects b-handles] 1]
 +                     ​[[b-requires a-handles] 1]
 +                     ​[[b-expects a-handles] -1]])
 +        (map (fn [[sets ret]]
 +               (and (seq (apply set/​intersection sets)) ret)))
 +        (some #{-1 1}))
 +      0))
 +</​code>​
 +
 +===== 미들웨어 함수들 =====
 +
 +==== session ====
 +==== add-stdin ====
 +==== wrap-describe ====
 +==== interruptible-eval ====
 +==== wrap-load-file ====
 +==== pr-values ====
  
lecture/nrepl/sources.1362474335.txt.gz · Last modified: 2019/02/04 14:26 (external edit)