用Scala實現簡單的Web和API伺服器

2020-12-06 CSDN

【CSDN 編者按】大家都知道Web和API伺服器在網際網路中的重要性,在計算機網絡方面提供了最基本的界面。本文主要介紹了怎樣利用Scala實現實時聊天網站和API伺服器,通過本篇文章,你定將受益匪淺。

作者 | Haoyi

譯者 | 彎月,責編 | 劉靜

以下為譯文:

Web和API伺服器是網際網路系統的骨幹,它們為計算機通過網絡交互提供了基本的界面,特別是在不同公司和組織之間。這篇指南將向你介紹如何利用Scala簡單的HTTP伺服器,來提供Web內容和API。本文還會介紹一個完整的例子,告訴你如何構建簡單的實時聊天網站,同時支持HTML網頁和JSON API端點。

這篇文及章的目的是介紹怎樣用Scala實現簡單的HTTP伺服器,從而提供網頁服務,以響應API請求。我們會建立一個簡單的聊天網站,可以讓用戶發表聊天信息,其他訪問網站的用戶都可以看見這些信息。為簡單起見,我們將忽略認證、性能、用戶掛曆、資料庫持久存儲等問題。但是,這篇文章應該足夠你開始用Scala構建網站和API伺服器了,並為你學習並構建更多產品級項目打下基礎。

我們將使用Cask web框架:

http://www.lihaoyi.com/cask/

Cask是一個Scala的HTTP為框架,可以用來架設簡單的網站並迅速運行。

開始

要開始使用Cask,只需下載並解壓示例程序:

$ curl -L https://github.com/lihaoyi/cask/releases/download/0.3.0/minimalApplication-0.3.0.zip > cask.zip$ unzip cask.zip$ cd minimalApplication-0.3.0

運行find來看看有哪些文件:

$ find . -type f./build.sc./app/test/src/ExampleTests.scala./app/src/MinimalApplication.scala./mill

我們感興趣的大部分代碼都位於app/src/MinimalApplication.scala中。

package appobject MinimalApplication extends cask.MainRoutes{@cask.get("/")defhello() = {"Hello World!" } @cask.post("/do-thing")defdoThing(request: cask.Request) = { new String(request.readAllBytes()).reverse } initialize()}

用build.sc進行構建:

import mill._, scalalib._object app extends ScalaModule{defscalaVersion = "2.13.0"defivyDeps = Agg(ivy"com.lihaoyi::cask:0.3.0" ) object test extends Tests{deftestFrameworks = Seq("utest.runner.Framework")defivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.1", ivy"com.lihaoyi::requests::0.2.0", ) }}

如果你使用Intellij,那麼可以運行如下命令來設置Intellij項目配置:

$ ./mill mill.scalalib.GenIdea/idea

現在你可以在Intellij中打開minimalApplication-0.3.0/目錄,查看項目的目錄,也可以進行編輯。

可以利用Mill構建工具運行該程序,只需執行./mill:

$ ./mill -w app.runBackground

該命令將在後臺運行Cask Web伺服器,同時監視文件系統,如果文件發生了變化,則重啟伺服器。然後我們可以使用瀏覽器瀏覽伺服器,默認網址是localhost:8080:

在/do-thing上還有個POST端點,可以在另一個終端上使用curl來訪問:

$ curl -X POST --data hello http://localhost:8080/do-thingolleh

可見,它接受數據hello,然後將反轉的字符串返回給客戶端。

然後可以運行app/test/src/ExampleTests.scala中的自動化測試:

$ ./mill clean app.runBackground # stop the webserver running in the background$ ./mill app.test[50/56] app.test.compile[info] Compiling 1 Scala source to /Users/lihaoyi/test/minimalApplication-0.3.0/out/app/test/compile/dest/classes ...[info] Done compiling.[56/56] app.test.test-------------------------------- Running Tests --------------------------------+ app.ExampleTests.MinimalApplication 629ms

現在基本的東西已經運行起來了,我們來重新運行Web伺服器:

$ ./mill -w app.runBackground

然後開始實現我們的聊天網站!

提供HTML服務

第一件事就是將純文本的"Hello, World!"轉換成HTML網頁。最簡單的方式就是利用Scalatags這個HTML生成庫。要在項目中使用Scalatags,只需將其作為依賴項加入到build.sc文件即可:

defivyDeps = Agg(+ ivy"com.lihaoyi::scalatags:0.7.0", ivy"com.lihaoyi::cask:0.3.0" )

如果使用Intellij,那麼還需要重新運行./mill mill.scalalib.GenIdea/idea命令,來發現依賴項的變動,然後重新運行./mill -w app.runBackground讓Web伺服器重新監聽改動。

然後,我們可以在MinimalApplication.scala中導入Scalatags:

packageapp+importscalatags.Text.all._objectMinimalApplicationextendscask.MainRoutes{

然後用一段最簡單的Scalatags HTML模板替換"Hello, World!"。

def hello() = {- "Hello World!"+ html(+ head(),+ body(+ h1("Hello!"),+ p("World")+ )+ ).render }

我們應該可以看到./mill -w app.runBackground命令重新編譯了代碼並重啟了伺服器。然後刷新網頁額,就會看到純文本已經被替換成HTML頁面了。

Bootstrap

為了讓頁面更好看一些,我們使用Bootstrap這個CSS框架。只需按照它的指南,使用link標籤引入bootstrap:

head(+ link(+ rel := "stylesheet", + href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"+ ) ),

body(- h1("Hello!"),- p("World")+ div(cls := "container")(+ h1("Hello!"),+ p("World")+ ) )

現在字體不太一樣了:

雖然還不是最漂亮的網站,但現在已經足夠了。

在本節的末尾,我們修改一下Scalatags的HTML模板,加上硬編碼的聊天文本和假的輸入框,讓它看起來更像一個聊天應用程式。

body( div(cls := "container")(- h1("Hello!"),- p("World")+ h1("Scala Chat!"),+ hr,+ div(+ p(b("alice"), " ", "Hello World!"),+ p(b("bob"), " ", "I am cow, hear me moo"),+ p(b("charlie"), " ", "I weigh twice as much as you")+ ),+ hr,+ div(+ input(`type` := "text", placeholder := "User name", width := "20%"),+ input(`type` := "text", placeholder := "Please write a message!", width := "80%")+ ) ) )

現在我們有了一個簡單的靜態網站,其利用Cask web框架和Scalatags HTML庫提供HTML網頁服務。現在的伺服器代碼如下所示:

package appimport scalatags.Text.all._object MinimalApplication extends cask.MainRoutes{@cask.get("/") def hello() = { html( head( link( rel := "stylesheet", href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" ) ), body( div(cls := "container")( h1("Scala Chat!"), hr, div( p(b("alice"), " ", "Hello World!"), p(b("bob"), " ", "I am cow, hear me moo"), p(b("charlie"), " ", "I weigh twice as much as you") ), hr, div( input(`type` := "text", placeholder := "User name", width := "20%"), input(`type` := "text", placeholder := "Please write a message!", width := "80%") ) ) ) ).render } initialize()}

接下來,我們來看看怎樣讓它支持交互!

表單和數據

為網站添加交互的第一次嘗試是使用HTML表單。首先我們要刪掉硬編碼的消息列表,轉而根據數據來輸出HTML網頁:

object MinimalApplication extends cask.MainRoutes{+ var messages = Vector(+ ("alice", "Hello World!"),+ ("bob", "I am cow, hear me moo"),+ ("charlie", "I weigh twice as much as you"),+ )@cask.get("/")

div(- p(b("alice"), " ", "Hello World!"),- p(b("bob"), " ", "I am cow, hear me moo"),- p(b("charlie"), " ", "I weight twice as much as you")+ for((name, msg) <- messages)+ yield p(b(name), " ", msg) ),

這裡我們簡單地使用了內存上的mssages存儲。關於如何將消息持久存儲到資料庫中,我將在以後的文章中介紹。

接下來,我們需要讓頁面底部的兩個input支持交互。為實現這一點,我們需要將它們包裹在form元素中:

hr,- div(- input(`type` := "text", placeholder := "User name", width := "20%"),- input(`type` := "text", placeholder := "Please write a message!", width := "80%")+ form(action := "/", method := "post")(+ input(`type` := "text", name := "name", placeholder := "User name", width := "20%"),+ input(`type` := "text", name := "msg", placeholder := "Please write a message!", width := "60%"),+ input(`type` := "submit", width := "20%") )

這樣我們就有了一個可以交互的表單,外觀跟之前的差不多。但是,提交表單會導致Error 404: Not Found錯誤。這是因為我們還沒有將表單與伺服器連接起來,來處理表單提交並獲取新的聊天信息。我們可以這樣做:

- )++ @cask.postForm("/")+ defpostHello(name: String, msg: String) = {+ messages = messages :+ (name -> msg)+ hello()+ }+ @cask.get("/")

@cast.postForm定義為根URL(即 / )添加了另一個處理函數,但該處理函數處理POST請求,而不處理GET請求。Cask文檔(http://www.lihaoyi.com/cask/)中還有關於@cask.*注釋的其他例子,你可以利用它們來定義處理函數。

驗證

現在,用戶能夠以任何名字提交任何評論。但是,並非所有的評論和名字都是有效的:最低限度,我們希望保證評論和名字欄位非空,同時我們還需要限制最大長度。

實現這一點很簡單:

@cask.postForm("/")defpostHello(name: String, msg: String) = {- messages = messages :+ (name -> msg)+ if (name != "" && name.length < 10 && msg != "" && msg.length < 160){+ messages = messages :+ (name -> msg)+ } hello() }

這樣就可以阻止用戶輸入非法的name和msg,但出現了另一個問題:用戶輸入了非法的名字或信息並提交,那麼這些信息就會消失,而且不會為錯誤產生任何反饋。解決方法是,給hello()頁面渲染一個可選的錯誤信息,用它來告訴用戶出現了什麼問題:

@cask.postForm("/")defpostHello(name: String, msg: String) = {- if (name != "" && name.length < 10 && msg != "" && msg.length < 160){- messages = messages :+ (name -> msg)- }- hello()+ if (name == "") hello(Some("Name cannot be empty"))+ elseif (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"))+ elseif (msg == "") hello(Some("Message cannot be empty"))+ elseif (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"))+ else {+ messages = messages :+ (name -> msg)+ hello()+ } }

@cask.get("/")- defhello() = {+ defhello(errorOpt: Option[String] = None) = { html(

hr,+ for(error <- errorOpt) + yield i(color.red)(error), form(action := "/", method := "post")(

現在,當名字或信息非法時,就可以正確地顯示出錯誤信息了。

下一次提交時錯誤信息就會消失。

記住名字和消息

現在比較煩人的是,每次向聊天室中輸入消息時,都要重新輸入用戶名。此外,如果用戶名或信息非法,那消息就會被清除,只能重新輸入並提交。可以讓hello頁面處理函數來填充這些欄位,這樣就可以解決:

@cask.get("/")- def hello(errorOpt: Option[String] = None) = {+ def hello(errorOpt: Option[String] = None, + userName: Option[String] = None,+ msg: Option[String] = None) = { html(

form(action := "/", method := "post")(- input(`type` := "text", name := "name", placeholder := "User name", width := "20%", userName.map(value := _)),- input(`type` := "text", name := "msg", placeholder := "Please write a message!", width := "60%"),+ input(+ `type` := "text", + name := "name", + placeholder := "User name", + width := "20%", + userName.map(value := _)+ ),+ input(+ `type` := "text",+ name := "msg",+ placeholder := "Please write a message!", + width := "60%",+ msg.map(value := _)+ ), input(`type` := "submit", width := "20%")

這裡我們使用了可選的userName和msg查詢參數,如果它們存在,則將其作為HTML input標籤的value的默認值。

接下來在postHello的處理函數中渲染頁面時,填充userName和msg,再發送給用戶:

defpostHello(name: String, msg: String) = {- if (name == "") hello(Some("Name cannot be empty"))- elseif (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"))- elseif (msg == "") hello(Some("Message cannot be empty"))- elseif (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"))+ if (name == "") hello(Some("Name cannot be empty"), Some(name), Some(msg))+ elseif (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"), Some(name), Some(msg))+ elseif (msg == "") hello(Some("Message cannot be empty"), Some(name), Some(msg))+ elseif (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"), Some(name), Some(msg))else {messages = messages :+ (name -> msg)- hello()+ hello(None, Some(name), None) }

注意任何情況下我們都保留name,但只有錯誤的情況才保留msg。這樣做是正確的,因為我們只希望用戶在出錯時才進行編輯並重新提交。

完整的代碼MinimalApplication.scala如下所示:

package appimport scalatags.Text.all._object MinimalApplication extends cask.MainRoutes{var messages = Vector(("alice", "Hello World!"), ("bob", "I am cow, hear me moo"), ("charlie", "I weigh twice as you"), ) @cask.postForm("/") def postHello(name: String, msg: String) = {if (name == "") hello(Some("Name cannot be empty"), Some(name), Some(msg))elseif (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"), Some(name), Some(msg))elseif (msg == "") hello(Some("Message cannot be empty"), Some(name), Some(msg))elseif (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"), Some(name), Some(msg))else { messages = messages :+ (name -> msg) hello(None, Some(name), None) } } @cask.get("/") def hello(errorOpt: Option[String] = None, userName: Option[String] = None, msg: Option[String] = None) = { html( head( link( rel := "stylesheet", href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" ) ), body( div(cls := "container")( h1("Scala Chat!"), hr, div(for((name, msg) <- messages) yield p(b(name), " ", msg) ), hr,for(error <- errorOpt) yield i(color.red)(error), form(action := "/", method := "post")( input(`type` := "text", name := "name", placeholder := "User name", width := "20%", userName.map(value := _) ), input(`type` := "text", name := "msg", placeholder := "Please write a message!", width := "60%", msg.map(value := _) ), input(`type` := "submit", width := "20%") ) ) ) ).render } initialize()}

利用Ajax實現動態頁面更新

現在有了一個簡單的、基於表單的聊天網站,用戶可以發表消息,其他用戶加載頁面即可看到已發表的消息。下一步就是讓網站變成動態的,這樣用戶不需要刷新頁面就能發表消息了。

為實現這一點,我們需要做兩件事情:

允許HTTP伺服器發送網頁的一部分,例如接收消息並渲染消息列表,而不是渲染整個頁面

添加一些Javascript來手動提交表單數據。

渲染頁面的一部分

要想只渲染需要更新的那部分頁面,我們可以重構下代碼,從hello頁面處理函數中提取出messageList()輔助函數:

)+ + def messageList() = {+ frag(+ for((name, msg) <- messages)+ yield p(b(name), " ", msg)+ )+ }+ @cask.postForm("/")

hr,- div(- for((name, msg) <- messages)- yield p(b(name), " ", msg)+ div(id := "messageList")(+ messageList() ),

接下來,我們可以修改postHello處理函數,從而僅渲染可能發生了變化的messageList,而不是渲染整個頁面:

- @cask.postForm("/")- defpostHello(name: String, msg: String) = {- if (name == "") hello(Some("Name cannot be empty"), Some(name), Some(msg))- elseif (name.length >= 10) hello(Some("Name cannot be longer than 10 characters"), Some(name), Some(msg))- elseif (msg == "") hello(Some("Message cannot be empty"), Some(name), Some(msg))- elseif (msg.length >= 160) hello(Some("Message cannot be longer than 160 characters"), Some(name), Some(msg))- else {- messages = messages :+ (name -> msg)- hello(None, Some(name), None)+ @cask.postJson("/")+ defpostHello(name: String, msg: String) = {+ if (name == "") ujson.Obj("success" -> false, "txt" -> "Name cannot be empty")+ elseif (name.length >= 10) ujson.Obj("success" -> false, "txt" -> "Name cannot be longer than 10 characters")+ elseif (msg == "") ujson.Obj("success" -> false, "txt" -> "Message cannot be empty")+ elseif (msg.length >= 160) ujson.Obj("success" -> false, "txt" -> "Message cannot be longer than 160 characters")+ else {+ messages = messages :+ (name -> msg)+ ujson.Obj("success" -> true, "txt" -> messageList().render)} }

注意我們這裡用@cask.postJson替換了@cask.postForm,此外不再調用hello()來重新渲染整個頁面,而是僅返回一個很小的JSON結構ujson.Obj,這樣瀏覽器可以利用它更新HTML頁面。ujson.Obj數據類型由uJson庫提供。

利用Javascript更新頁面

現在我們寫好了伺服器端代碼,接下來我們編寫相關的客戶端代碼,從伺服器接收JSON響應,並利用它來更新HTML界面

要處理客戶端邏輯,我們需要給一些關鍵的HTML元素添加ID,這樣才能在Javascript中訪問它們:

hr,- for(error <- errorOpt)- yield i(color.red)(error),+ div(id := "errorDiv", color.red), form(action := "/", method := "post")(

input(`type` := "text",- name := "name",+ id := "nameInput", placeholder := "User name", width := "20%" ), input(`type` := "text",- name := "msg",+ id := "msgInput", placeholder := "Please write a message!", width := "60%" ),

接下來,在頁面頭部引入一系列Javascript:

head( link( rel := "stylesheet", href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"- ),+ )+ script(raw("""+ function submitForm(){+ fetch(+ "/",+ {+ method: "POST",+ body: JSON.stringify({name: nameInput.value, msg: msgInput.value})+ }+ ).then(response => response.json())+ .then(json => {+ if (json.success) {+ messageList.innerHTML = json.txt+ msgInput.value = ""+ errorDiv.innerText = ""+ } else {+ errorDiv.innerText = json.txt+ }+ })+ }+ """)) ),

從表單的onsubmit處理函數中調用該Javascript函數:

- form(action := "/", method := "post")(+ form(onsubmit := "submitForm(); return false")(

這樣就可以了。現在向網站添加聊天文本,文本就會立即出現在網頁上,之後加載頁面的其他人也能看見。

最終的代碼如下:

package appimport scalatags.Text.all._object MinimalApplication extends cask.MainRoutes{var messages = Vector( ("alice", "Hello World!"), ("bob", "I am cow, hear me moo"), ("charlie", "I weigh twice as you"), )defmessageList() = { frag(for((name, msg) <- messages)yield p(b(name), " ", msg) ) } @cask.postJson("/")defpostHello(name: String, msg: String) = {if (name == "") ujson.Obj("success" -> false, "txt" -> "Name cannot be empty")elseif (name.length >= 10) ujson.Obj("success" -> false, "txt" -> "Name cannot be longer than 10 characters")elseif (msg == "") ujson.Obj("success" -> false, "txt" -> "Message cannot be empty")elseif (msg.length >= 160) ujson.Obj("success" -> false, "txt" -> "Message cannot be longer than 160 characters")else { messages = messages :+ (name -> msg) ujson.Obj("success" -> true, "txt" -> messageList().render) } } @cask.get("/")defhello() = { html( head( link( rel := "stylesheet", href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" ), script(raw(""" function submitForm(){ fetch( "/", { method: "POST", body: JSON.stringify({name: nameInput.value, msg: msgInput.value}) } ).then(response => response.json()) .then(json => { if (json.success) { messageList.innerHTML = json.txt msgInput.value = "" errorDiv.innerText = "" } else { errorDiv.innerText = json.txt } }) } """)) ), body( div(cls := "container")( h1("Scala Chat!"), hr, div(id := "messageList")( messageList() ), hr, div(id := "errorDiv", color.red), form(onsubmit := "submitForm(); return false")( input(`type`:= "text", id := "nameInput", placeholder := "User name", width := "20%" ), input(`type`:= "text", id := "msgInput", placeholder := "Please write a message!", width := "60%" ), input(`type`:= "submit", width := "20%") ) ) ) ).render } initialize()}

注意儘管你輸入的消息你自己可以立即看到,但其他人只有刷新頁面,或者輸入自己的消息迫使messageList重新加載,才能看到你的消息。本文的最後一節將介紹怎樣讓所有人立即看到你的消息,而不需要手動刷新。

利用Websockets實時更新頁面

推送更新的概念和簡單:每次提交新消息後,就將消息」推送"到所有監聽中的瀏覽器上,而不是等待瀏覽器刷新並「拉取」更新後的數據。實現這一目的有多種方法。本文我們使用Websockets。

Websockets可以讓瀏覽器和伺服器在正常的HTTP請求-響應流之外互相發送消息。連接一旦建立,任何一方都可以在任何時間發送消息,消息可以包含任意字符串或任意字節。

我們要實現的流程如下:

網站加載後,瀏覽器建立到伺服器的websocket連接連接建立後,瀏覽器將發送消息"0"到伺服器,表明它已準備好接收更新伺服器將響應初始的txt,其中包含所有已經渲染的消息,以及一個index,表示當前的消息計數每當收到消息時,瀏覽器就會將最後看到的index發送給伺服器,然後等待新消息出現,再按照步驟3進行響應在伺服器上實現這一點的關鍵就是保持所有已打開的連接的集合:

var openConnections = Set.empty[cask.WsChannelActor]

該集合包含當前等待更新的瀏覽器的列表。每當新消息出現時,我們就會向這個列表進行廣播。

接下來,定義@cask.websocket處理函數,接收進入的websocket連接並處理:

@cask.websocket("/subscribe") def subscribe() = { cask.WsHandler { connection => cask.WsActor {case cask.Ws.Text(msg) =>if (msg.toInt < messages.length){ connection.send( cask.Ws.Text( ujson.Obj("index" -> messages.length, "txt" -> messageList().render).render() ) ) }else{ openConnections += connection }case cask.Ws.Close(_, _) => openConnections -= connection } } }

該處理函數接收來自瀏覽器的msg,檢查其內容是否應該立即響應,還是應該利用openConnections註冊一個連接再稍後響應。

我們需要在postHello處理函數中做類似的修改:

messages = messages :+ (name -> msg)+ val notification = cask.Ws.Text(+ ujson.Obj("index" -> messages.length, "txt" -> messageList().render).render()+ )+ for(conn <- openConnections) conn.send(notification)+ openConnections = Set.emptyujson.Obj("success" -> true, "txt" -> messageList().render)

這樣,每當新的聊天消息提交時,就會將它發送給所有打開的連接,以通知它們。

最後,我們需要在瀏覽器的script標籤中添加一點Javascript代碼,來打開Websocket連接,並處理消息的交換:

var socket = new WebSocket("ws://" + location.host + "/subscribe");var eventIndex = 0socket.onopen = function(ev){ socket.send("" + eventIndex) }socket.onmessage = function(ev){var json = JSON.parse(ev.data)eventIndex = json.index socket.send("" + eventIndex) messageList.innerHTML = json.txt}

這裡,我們首先打開一個連接,發送第一條"0"消息來啟動整個流程,然後每次收到更新後,就將json.txt渲染到messageList中,然後將json.index發送回伺服器,來訂閱下一次更新。

現在,同時打開兩個瀏覽器,就會看到一個窗口中發送的聊天消息立即出現在另一個窗口中:

本節的完整代碼如下:

package appimport scalatags.Text.all._object MinimalApplication extends cask.MainRoutes{var messages = Vector( ("alice", "Hello World!"), ("bob", "I am cow, hear me moo"), ("charlie", "I weigh twice as you"), ) var openConnections = Set.empty[cask.WsChannelActor]defmessageList() = { frag(for((name, msg) <- messages)yield p(b(name), " ", msg) ) } @cask.postJson("/")defpostHello(name: String, msg: String) = {if (name == "") ujson.Obj("success" -> false, "txt" -> "Name cannot be empty")elseif (name.length >= 10) ujson.Obj("success" -> false, "txt" -> "Name cannot be longer than 10 characters")elseif (msg == "") ujson.Obj("success" -> false, "txt" -> "Message cannot be empty")elseif (msg.length >= 160) ujson.Obj("success" -> false, "txt" -> "Message cannot be longer than 160 characters")else { messages = messages :+ (name -> msg) val notification = cask.Ws.Text( ujson.Obj("index" -> messages.length, "txt" -> messageList().render).render() )for(conn <- openConnections) conn.send(notification) openConnections = Set.empty ujson.Obj("success" -> true, "txt" -> messageList().render) } } @cask.get("/")defhello() = { html( head( link( rel := "stylesheet", href := "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" ), script(raw(""" function submitForm(){ fetch( "/", { method: "POST", body: JSON.stringify({name: nameInput.value, msg: msgInput.value}) } ).then(response => response.json()) .then(json => { if (json.success) { messageList.innerHTML = json.txt msgInput.value = "" errorDiv.innerText = "" } else { errorDiv.innerText = json.txt } }) } var socket = new WebSocket("ws://" + location.host + "/subscribe"); socket.onopen = function(ev){ socket.send("0") } socket.onmessage = function(ev){ var json = JSON.parse(ev.data) messageList.innerHTML = json.txt socket.send("" + json.index) } """)) ), body( div(cls := "container")( h1("Scala Chat!"), hr, div(id := "messageList")( messageList() ), hr, div(id := "errorDiv", color.red), form(onsubmit := "submitForm(); return false")( input(`type`:= "text", id := "nameInput", placeholder := "User name", width := "20%" ), input(`type`:= "text", id := "msgInput", placeholder := "Please write a message!", width := "60%" ), input(`type`:= "submit", width := "20%") ) ) ) ).render } @cask.websocket("/subscribe")defsubscribe() = { cask.WsHandler { connection => cask.WsActor {case cask.Ws.Text(msg) =>if (msg.toInt < messages.length){ connection.send( cask.Ws.Text( ujson.Obj("index" -> messages.length, "txt" -> messageList().render).render() ) ) }else{ openConnections += connection }case cask.Ws.Close(_, _) => openConnections -= connection } } } initialize()}

總結

本文我們介紹了怎樣利用Scala實現實時聊天網站和API伺服器。我們從靜態網站開始,添加基於表單的交互,再利用Ajax訪問JSON API實現動態頁面,最後利用websocket實現推送通知。我們使用了Cask web框架,Scalatags HTML庫,以及uJson序列化庫,代碼大約125行。

這裡展示的聊天網站非常簡單。我們故意忽略了將消息保存到持久資料庫、認證、用戶帳號、多聊天室、使用量限制以及許多其他的功能。這裡僅使用了內存上的messages列表和openConnections集合,從並發更新的角度來看,它們並非線程安全的。但無論如何,希望這篇文章能夠讓你體會到怎樣使用Scala實現簡單的網站和API伺服器,進而用它構建更大、更宏偉的應用程式。

英文:Simple Web and Api Servers with Scala

原文連結:http://www.lihaoyi.com/post/SimpleWebandApiServerswithScala.html

相關焦點

  • 基於Django和翻譯API實現web版的中英文對照翻譯(一)
    一番了解之後,決定選用谷歌翻譯/搜狗翻譯/有道翻譯官方提供的翻譯api自己實現一個web版的翻譯界面。目前搜狗翻譯/有道翻譯都已經開始收費,但收費的標準還可以讓人接受,以搜狗翻譯為例,現價:40.00/百萬字符。還是可以接受的。閒言少敘。直入正題。
  • 基於Android的嵌入式Web伺服器設計
    本文主要論述了基於Android系統環境,在家庭網關中實現嵌入式Web伺服器的設計方法,介紹了i-jetty嵌入式Web伺服器,及其Web應用功能的實現。關鍵詞:Android;嵌入式Web伺服器;i-jetty;SQLite 只要在嵌入式設備中集成了Web伺服器,就能實現用戶與嵌入式設備低成本、高通用性的信息交流,即客戶端利用HTTP瀏覽器,在任何時間、任何地點都能實現與嵌入式設備的信息交互。可以說,嵌入式Web的應用極大地促進嵌入式設備,特別是家電設備、通信終端、儀器儀表的信息交互和遠程控制功能。
  • Azure 靜態 web 應用集成 Azure 函數 API
    但是一個真正的web應用,總是免不了需要後臺api服務為前端提供數據或者處理數據的能力。同樣前面我們也介紹了Azure函數服務,Azure函數的http trigger可以對http作出響應,可以完美的承當web api的角色。現在Azure靜態web應用可以直接集成Azure函數,使得一次發布可以同時發布前端項目(vue、blazor)及後臺api服務(azure函數)。
  • PySpark源碼解析,用Python調用高效Scala接口,搞定大規模數據分析
    而 PythonRDD (core/src/main/scala/org/apache/spark/api/python/PythonRDD.scala),則是一個 Scala 中封裝的伴生對象,提供了常用的 RDD IO 相關的接口。另外一些接口會通過 self._jsc 對象去創建 RDD。其中 self._jsc 就是 JVM 中的 SparkContext 對象。
  • web伺服器性能對比
    一般來說,4GB內存的伺服器+Apache(prefork模式)一般只能處理3000個並發連接,因為它們將佔用3GB以上的內存,還得為系統預留1GB的內存。我曾經就有兩臺Apache伺服器,因為在配置文件中設置的MaxClients為4000,當Apache並發連接數達到3800時,導致伺服器內存和Swap空間用滿而崩潰。
  • 對常見的WEB伺服器和應用伺服器的介紹
    它提供ISAPI(Intranet Server API)作為擴展Web伺服器功能的編程接口;同時,它還提供一個Internet資料庫連接器,可以實現對資料庫的查詢和更新。② IBM WebSphereWebSphere Application Server 是 一 種功能完善、開放的Web應用程式伺服器,是IBM電子商務計劃的核心部分,它是基於 Java 的應用環境,用於建立、部署和管理 Internet 和 Intranet Web 應用程式。 這一整套產品進行了擴展,以適應 Web 應用程式伺服器的需要,範圍從簡單到高級直到企業級。
  • Apache和IIS及nginx三大web伺服器,新手站長該如何選擇?
    網站上線時第一件事就是搭建運行環境,首先要選擇的就在伺服器上使用哪一個web伺服器,現在win系統默認自帶IIS而Linux則自帶Apache,如果需要使用nginx則需要單獨安裝。困擾新手站長的就是web服務到底該使用哪一個,目前流行的3大web伺服器有哪些優劣請看使用經驗。
  • 如何配置web伺服器
    如何配置web伺服器?在伺服器上配置Web服務,首先需要安裝網絡環境,然後上傳web項目文件,在配置web服務時,有一些安全策略也要注意。1 啟用日誌記錄功能Web伺服器應配置日誌功能,對用戶登錄進行記錄,記錄內容包括用戶登錄使用的帳號、登錄是否成功、登錄時間以及遠程登錄時用戶使用的IP位址。
  • WindowServer2003伺服器搭建WEB伺服器
    首先選擇伺服器硬體品牌和伺服器作業系統,一、下面首先介紹一下伺服器作業系統。 WindowsServer2003是微軟於2003年4月底上市發行的伺服器作業系統,分為幾個不同的版本,具有不同的功能和用途。
  • 使用C#的後端Web API:循序漸進教程
    也許有人會推薦PHP,它具有豐富的功能和較低的學習曲線。然而,事實仍然是現在最常用的語言是Java和.NET。本教程介紹如何使用C#(ASP.NET)構建自己的Web伺服器(Web API)。重要的是要注意,要託管您的伺服器,您將需要基於Windows的託管。
  • api框架 web 最好的go_golang api框架 - CSDN
    其實對於golang而言,web框架的依賴要遠比Python,Java之類的要小。自身的net/http足夠簡單,性能也非常不錯。框架更像是一些常用函數或者工具的集合。藉助框架開發,不僅可以省去很多常用的封裝帶來的時間,也有助於團隊的編碼風格和形成規範。下面就Gin的用法做一個簡單的介紹。
  • 深入研究嵌入式web伺服器視頻監控的應用
    本文根據監控系統對敖據吞吐量和安全可靠性等各方面的實際要求,結合相關研究的新進展,深入討論了web伺服器在監控系統設計中的應用技巧,並詳細做了實現上的闡述。對所有基於嵌入式web技術的監控系統的設計具有非常實際的指導作用。
  • 005精讀 | 4萬高贊神文:什麼是API?
    First, let’s pull back and look at how the web itself works.但是如何用簡單的英語解釋API呢?還有比開發和業務中使用的含義更廣泛的含義嗎?首先,讓我們回顧一下web本身是如何工作的。
  • 國產開源web伺服器kangle 穩定版2.6.1
    kangle web伺服器是一款國產開源的高性能web伺服器和反向代理伺服器軟體;帶有簡單操作的web控制臺。
  • 資源 從人臉識別到機器翻譯:52個有用的機器學習和預測API
    連結:https://www.microsoft.com/cognitive-services/en-us/computer-vision-api11.Rekognition:為社交圖片應用提供面部和場景的識別和優化。Rekognition API 可以利用眼睛、嘴、鼻子和面部的特徵實現情緒識別和性別檢測,可以用來確定性別、年齡和情緒。
  • kangle 3.4.8 發布,國產開源 Web 伺服器
    kangle web伺服器是一款國產開源的高性能web伺服器和反向代理伺服器軟體;集成簡單易操作的web控制臺。
  • 你知道網際網路有哪些常用的web伺服器嗎?看有沒有你的常用的
    說到做網站你知道的web伺服器有哪些呢?一般網際網路上的網站都是採用哪些web伺服器呢?首先我們來了解下什麼是web伺服器, 顧名思義Web 伺服器就是提供web服務的伺服器,也可以叫 web server 比如我們經常用到的搜尋引擎百度就是一個典型的web伺服器例子。
  • 令人激動的新興 Web 技術:WebGL和SVG
    gUM 允許訪問用戶的攝像頭和麥克風,本來是在 WebRTC 規範中在瀏覽器中進行 P2P 視頻會議的,當 gUM 擁有了其他的用途,就離開了 WebRTC。  攝像頭的訪問最終在 Opera12 安卓版,Opera 桌面實驗室和 Google Chrome Canary 裡面實現了,不過 Opera 和 Chrome 都還沒有實現麥克風的接入。
  • 科普應用伺服器,與Web伺服器有啥區別?
    【IT168 資訊】它位於網絡和資料庫之間,那麼應用伺服器實際上是做什麼的?應用程式伺服器是為應用程式提供業務邏輯的代碼。它是基於組件的,位於以伺服器為中心的架構的中間層。這個架構主要基於Web。中間層是業務邏輯所在的應用伺服器。而第三層,則是事務伺服器的資料庫。
  • 應用伺服器是什麼_應用伺服器有哪些
    1、定義   應用伺服器是指通過各種協議把商業邏輯曝露給客戶端的程序。它提供了訪問商業邏輯的途徑以供客戶端應用程式使用。應用伺服器使用此商業邏輯就像調用對象的一個方法一樣。   簡單的說,能實現動態網頁技術的伺服器叫做應用伺服器。