Ring을 공부해보자
여기 트리에서는 아래 순서로 정리한다.
(주요 내용은 모두 다 https://github.com/ring-clojure/ring/wiki - Ring의 위키 페이지에서 가져왔다 )
거기에는 세가지 개념이 있다
Ring에서 http request는 clojure map으로, http response또한 clojure map으로 표현이된다.
여기 기본중의 기본으로 request에 어떤 내용이 실려서 전달되는지, 그리고 response로 돌려줄 때 어떤 내용을 채워서 줘야하는지 알아보자
Ring에서 여러가지 유용한 함수모음들을 제공해준다
Ring에서 기본적으로 필요한 middlewares를 제공한다
handler는 웹 request를 처리하는 녀석이다
입력 ->
clojure map을 받아서
출력 ->
clojure map을 돌려준다
handler에다가 무슨 기능을 덧붙이고 싶을 때 사용한다.
handler가 요청을 처리하기 전/후로 해서 추가로 기능을 처리할 수가 있다
ring 코드의 많은 부분이 middleware가 차지한다.
middleware가 없으면 web 개발을 하지 말라는 소리와 같다
(왜냐하면 너무나 유용한 기능이 많이 있으니까)
adapter는 http protocol을 알아서 처리해주는 역할을 한다.
기본적으로 ring에서는 jetty-adapter를 전달해준다
adapter가 처리해서 주는 request map에는 기본적으로 아래와 같은 항목들이 채워져 있다
:server-port
- request가 처리되고 있는 port:server-name
- 서버 이름 또는 ip:remote-addr
- 요청을 마지막으로 보낸 녀석의 ip address:uri
- 요청된 URI, 앞에 host 이름을 빠져있다:query-string
- 있으면 query string이 보여진다:scheme
- 전송 프로토콜 ( :http
아니면 :https
) 둘중에 하나이다:request-method
- 요청 방법 (:get
, :head
, :options
, :put
, :post
, :delete
) 얘네들 중에 하나이다:content-type
- request body에 대한 mime type이다:content-length
- request body에 대한 길이:character-encoding
- request body에 사용된 character encoding (만약 사용되어져 있다면):headers
- clojure map으로 되어있는 headers:body
- input stream이다handler가 돌려주는 response는 항상 아래 3개의 항목을 담고 있어야한다
:status
- HTTP 상태코드:headers
- clojure map으로 되어있는 헤더 정보, request 일 때와는 다르게 특별히 대소문자를 중간에서 처리하지 않는다. 흠. 정말????:body
- 응답 내용Ring response는 단순 map이기 때문에 쉽게 만들 수 있지만 utilities 함수를 이용해서 더 빠르게 만들 수 있다. Ring이 제공하는 response 관련 utility 함수들 목록은 아래와 같다
일반 파일들을 그대로 제공하는 기능은 두가지 middleware에 의해서 제공된다.
아래 함수는 파일을 serving할 때 추가로 유용한 기능을 제공한다
이 middleware를 사용하면 URI에 있는 파일의 확장자를 가지고 알맞은 mime type을 결정해준다. 추가로 custom mime type을 추가할 수가 있다.
(use 'ring.middleware.content-type)
(def app
(wrap-content-type your-handler))
웹브라우져에서 서버쪽으로 추가로 필요한 데이터들을 파라미터로 담아서 보내준다. ring.middleware.params/wrap-params는 이 파라미터들을 처리해서 clojure map으로 변경시켜준다.
:query-params
로:form-params
로:params
에는 2개가 merge된 값이 들어있다.쿠키는 wrap-cookie
미들웨어를 사용한다
:cookies라는 키를 추가한다.
(use 'ring.middleware.cookies)
(def app
(wrap-cookies your-handler))
세션은 wrap-session
을 이용한다
미들웨어 중에서 제일 복잡한 것 중의 하나이다.
wrap-session
은 두가지 일을 한다
파일 올리는 기능은 wrap-multipart-params
에서 담당을 한다. 기본적으로 올라온 파일은 임시 폴더에 저장이 되고 한시간 후에 삭제당한다
(use 'ring.middleware.params
'ring.middleware.multipart-params)
(def app
(-> your-handler
wrap-params
wrap-multipart-params))
세가지 방식으로 제작할 수가 잇다.
(defn what-is-my-ip [request]
{:status
:headers {"Content-Type" "text/plain"}
:body (:remote-addr request)})
헤더는 clojure map으로 주어진다.
그리고 키 값은 언제나 소문자로 된 string이 주어진다
http response에 담겨질 수 있는 4가지의 데이터 type이 있다
String
- 문자열, 담으면 그대로 간다ISeq
- Sequence의 각 엘리먼트가 클라이언트에 전달된다. 음 아직 이부분이 명쾌하지 않다.File
- 참조되는 파일의 내용이 전달된다InputStream
- 입력 stream의 내용이 그대로 전송된다.기본적인 response map을 생성한다
(response "Hello World")
=>
{ :status 200
:headers {}
:body "Hello World"
}
response map에다가 주어진 content-type 헤더를 추가한다
(->
(response "Hello World")
(content-type "text/plain"))
=>
{ :status 200
:headers { "Content-Type" "text/plain" }
:body "Hello ,World"}
Redirection을 시키는 response를 생성한다
(redirect "http://www.google.com")
=>
{:status 302
:headers {"Location" "http://www.google.com"}
:body ""}
303 Redirection코드를 갖는 map을 생성한다
(redirect-after-post "http://www.google.com")
=>
{:status 303
:headers {"Location" "http://www.google.com"}
:body ""}
not found 에러를 갖는 response를 보여준다
(not-found "don_t know")
=>
{:status 404
:headers {}
:body "donr_knwo"]
파일 시스템에 있는 파일을 돌려주는 response를 만든다
(file-response "readme.html" {:root "public"})
=>
{:status 200
:headers {}
:body (io/file "public/readme.html")}
jar 파일안에 포함된 리소스 폴더 아래에 있는 파일 내용을 돌려준다.
(resource-response “readme.html” {:root “public”})
=>
{:status 200
:headers {}
:body (io/input-stream (io/resource “public/readme.html”))}
리소스 폴더는 clojure project 폴더 아래에 resource라는 폴더이름으로 존재한다.
(-> (response "Hello, World")
(status 201))
=>
{:status 201
:headers {}
:body "Hello, World"}
Response에 새로운 헤더 값을 붙여서 돌려준다
(-> (response "Hello, World")
(header "sample" "123"))
=>
{:status 200
:headers {"sample" "123"}
:body "Hello, World"}
Header에 있는 Content-Type에다가 charset 항목을 추가한다.
???
cookie를 설정한다고 하는데, 이거는 잘 모르겠다. 지금은.
주어진 map이 response용으로 작성된 map인지 여부를 체크한다
wrap-file은 시스템 path에 있는 파일을 불러들여서 serving한다
(use 'ring.middleware/file)
(def app
(wrap-file your-handler "/var/www/public"))
/var/www/public에 요청된 파일이 있다면 그 파일을 먼저 serve한다. 파일이 발견되지 않으면 그 다음 핸들러에게 처리를 넘긴다.
wrap-resource는 jar파일에 같이 묶여있는 리소스 폴더 아래에 있는 내용을 가지고 serve한다.
(use 'ring.middleware.resource)
(def app
(wrap-resource your-handler "public"))
리소스 폴더 안에 public 폴더를 root로 해서 요청되는 파일을 serve한다
이 함수는 파일에 대한 정보를 이용해서 마지막 수정된 날짜, 그리고 Content-Type을 업데이트해준다.
대개 wrap-file / wrap-resource와 같이 쓰인다.
(use 'ring.middleware.resource
'ring.middleware.file-info)
(def app
(-> your_handler
(wrap-resource "public")
(wrap-file-info)))
(def app
(wrap-params your_handler))
로 지정한다
{:http-method get
:uri "/search"
:query-string "q=clojure"}
이렇게 들어온 요청이 middleware을 아래와 같이 변경한다
{:http-method get
:uri "/search"
:query-string "q=clojure"
:query-params {"q" "clojure"}
:form-params {}
:params {"q" "clojure"}}
쿠키 미들웨어를 추가하면
{
:status 200
:headers {}
:cookies {"username" {:value "alice}}
:body ""}
위의 처럼 cookie라는 항목을 추가한다
Response 쉽게 만들기에서 보았던 ring.utilities.response/set-cookie 함수를 이용해서 쿠키값을 설정한다
주의할 점은 헤더에 :cookies를 집어넣은 것이 아니다.
response map에다가 집어넣은 것이다.
(use 'ring.middleware.session
'ring.util.response)
(defn handler [{session :session}]
(response (str "Hello " (:username session))))
(def app
(wrap-session handler))
(defn handler [{session :session}]
(let [count (:count session 0)
session (assoc session :count
(inc count))]
(-> (response (str count " times"))
(assoc :session session))))
response map에다가 :session 키에다가 nil값을 설정한다.
(assoc session :session nil)
세션 속성 설정은 :cookie-attrs
를 이용한다. :cookies-attrs
를 키로하고 값은 clojure map을 갖는다
(def app
(wrap-session handler {:cookie-attrs {:max-age 3600 }}))
wrap-session
을 호출할 때 옵션항목을 붙여서 넘겨준다
session cookies를 secure하게 넘기길 원할 때는 아래와 같이 옵션으로 :secure를 설정한다
(def app
(wrap-session handler {:cookie-attrs {:secure true}}))
세션값은 저장되어야한다
ring에서 기본적으로 2가지 type의 세션 저장소를 제공한다
memory store는 메모리에 세션을 저장한다. 또 다른 안은 쿠키 않에 encrypt된 형태로 세션을 저장한다.
세션에 저장되는 기본 값은 메모리이다.
하지만 저장소를 다른 것으로 변경할 수 있다.
(use 'ring.middleware.session.cookie)
(def app
(wrap-session handler {:store (cookie-store {:key "a 16-byte secret"})}))
세션 저장소는를 따로 구현하려면 ring.middleware.session.store/SessionStore
프로토콜을 구현해야한다
(use 'ring.middleware.session.store)
(deftype CustomStore[]
SessionStore
(read-session [_ key]
(read-data key))
(write-session [_ key data]
(let [key (or key (generate-new-random-key))]
(save-data key data)
key))
(delete-session [_ key]
(delete-data key)
nil)
write-session
함수에서 새로운 세션의 경우 key값이 nil로 온다
key값을 정교하게 만들어야지 공격을 당했을 때 비밀을 보장할 수가 있다
파일 업로딩을 하게 되면 request에 :multipart-params
를 추가하게 되고 :params
에 merge를 시킨다
:mutipart-params
에 는 아래와 같은 항목들이 추가된다
:filename
:content-type
:tempfile
:size
복수의 파일이 올라오게 되면????
어떻게 되는 건지는 모르겠당.
아마도 :filename에 vector형식으로 모여지는 것으로 보인다.
코드 상으로는…
lein-ring은 leiningen의 플러그인이다.
설정 방법은 아래와 같다.
project.cli파일에 아래 내용을 추가한다.
:plugins [[lein-ring "0.8.7"]]
project.cli파일에 :ring
이라는 키를 추가하고 거기에 몇가지 설정 관련된 옵션들을 제공한다.
설정 옵션들에서 필요한 것은 최상위 핸들러 함수를 지정하는 것임.
:ring { :handler my-project.core/handler }
아래와 같이 command line에서 실행하면 서버가 실행되고 웹브라우져를 실행시킨 다음 페이지를 하나 연다.
lein ring server
이 코드는 더 이상 유지보수가 되지 않는 것으로 보인다. 이거 project.clj에 집어넣으면 hiccup 옛날 버젼을 자꾸 찾는다. ring-devel? 버젼을 옛날 것을 사용한다. 현재 2년 전 소스를 고친것이 마지막으로 변경한 것이되버렸다
Ring serve를 사용하는 이유는 IDE환경 내에서 서버를 띄우고 repl로 접속해서 이것저것 찔러보려고 할 때 사용한다. (Lein Ring으로 하게 되는 경우 repl을 찔러볼 수가 없다)
ring-serve
라이브러리를 사용해서 작업을 한다. project.clj에 아래 dependencies를 추가한다. 이 라이브러리는 development 단계에서 사용하게 되므로 아래와 같이 dev profile에 넣어준다. (leiningen 2.0이상에서 작동한다)
:profiles { :dev {:dependencies [[ring-serve "0.1.2"]] } }
dependency를 추가한 후에 lein deps
를 실행해서 필요한 라이브러리를 다운로드받아두자.
REPL을 사용해서 원하는 handler를 작동시킨다
user> (require 'your-app.core/handler)
nil
user> (use 'ring.util.serve)
nil
user> (serve your-app.core/handler)
Started web server on port 3000
이제부터 파일을 변경한 후 다시 변경된 파일을 repl로 불러들이면 변경된 내용이 자동으로 적용된다.
먼저번 lein ring이 더 실제적이다.
수동으로 하려면 아래와 같은 요건을 만족시켜야한다.
그러니까 아래처럼 하란 말이다.
user> (defonce server (run-jetty #'handler {:port 8080 :join? false}))
#'handler
가 포인트이다.