In this section, we code a web server that communicates with our clients and runs the todo
app; the todo
data is sent to and from the web server in the JSON string format. Spiral s06 consists of a server and a client part. To run it, first start the server (lib/server/server.dart
) in Dart Editor or from the console; it runs when you see in the server.dart
tab in Dart Editor: Listening for GET and POST on http://127.0.0.1:8080 (If it does not run, use run/manage launches). Then, start one or more clients (web
/app.html
) in Dartium. Locally, the client still saves the data in IndexedDB. Our screen has two new buttons:
This is how the tasks
application at this stage looks like:
The following is the client code (from lib/view/view.dart
) for To server (posting data):
ButtonElement toServer = querySelector('#to-server'), toServer.onClick.listen((MouseEvent e) { var request = new HttpRequest(); (1) request.onReadyStateChange.listen((_) { (2) if (request.readyState == HttpRequest.DONE && request.status == 200) { // Data saved OK on server. serverResponse = 'Server: ' + request.responseText; } else if (request.readyState == HttpRequest.DONE && request.status == 0) { // Status is 0...most likely the server isn't running. serverResponse = 'No server'; } }); var url = 'http://127.0.0.1:8080'; request.open('POST', url); (3) request.send(_tasksStore.tasks.toJsonString()); (4) });
In line (1)
, a new client request is made. From line (3)
, we see that the method is POST
. In line (4)
, the data from the tasks collection is sent to the server. Then, the client listens to a possible server response ( status
and responseText
) in the onReadyStateChange
event. HttpStatus 200
indicates that everything went fine. The code for fromServer
(getting data) is shown as follows:
ButtonElement fromServer = querySelector('#from-server'), fromServer.onClick.listen((MouseEvent e) { HttpRequest.getString('http://127.0.0.1:8080') .then((result) { (5) String jsonString = result; serverResponse = 'Server: ' + result; print('JSON text from the server: ${jsonString}'), if (jsonString != '') { List<Map> jsonList = JSON.decode(jsonString); (6) print('JSON list from the server: ${jsonList}'), _tasksStore.loadFromJson(jsonList) (7) .then((_) { var tasks = _tasksStore.tasks; _clearElements(); loadElements(tasks); }) .catchError((e) { print('error in loading data into IndexedDB from JSON list'), }); } }); });
We get the data in the JSON format from the server with the getString
method. The response from the server containing the data is stored in result
in line (5)
, decoded in List<Map>
in line (6)
, and added to IndexedDB through the loadFromJson
method (line (7)
). Of course, the print statements are only needed as a way to log what takes place and what can be left out. We will improve this code in Spiral s07.
However, what happens on the server? The server is started through the following code:
import 'dart:io'; import 'dart:convert'; const String HOST = "127.0.0.1"; // or: "localhost" const int PORT = 8080; List<Map> jsonList; void main() { start(); } start() { HttpServer.bind(HOST, PORT).then((server) { server.listen((HttpRequest request) { switch (request.method) { (1) case 'GET': handleGet(request); break; case 'POST': handlePost(request); break; case 'OPTIONS': handleOptions(request); break; default: defaultHandler(request); } }, onError: print); (2) }) .catchError(print) .whenComplete(() => print('Listening for GET and POST on http://$HOST:$PORT')); }
In line (1)
in the listen
handler, we match the method of the request. Notice in line (2)
, the onError
handler, which is, in fact, the second optional parameter of the listen
method (onError: print
could also be written as onError: (e) => print(e)
to see an error while trying to start the server on port 80
. Then, you get SocketException
). Everything between lines (1)
and (2)
is the (anonymous) onData
handler of listen
.
In the To server situation, handlePost
is executed:
void handlePost(HttpRequest request) { print('${request.method}: ${request.uri.path}'), request.listen((List<int> buffer) { (3) var jsonString = new String.fromCharCodes(buffer); jsonList = JSON.decode(jsonString); (4) print('JSON list in POST: ${jsonList}'), (5) }, onError: print); }
Here, in the listen
handler the client data is loaded in the buffer in line (3)
. It is then decoded to the List<Map> jsonList
variable on the server storing the data in memory (line (4)
). The server prints to its console in line (5)
:
POST: / JSON list in POST: [{title: washing dishes, completed: true, updated: 2013-08-08 15:40:51.999}, {title: walking the dog, completed: true, updated: 2013-08-08 10:30:47.794}, {title: cleaning the kitchen, completed: false, updated: 2013-08-08 15:21:44.626}, {title: buying vegetables, completed: false, updated: 2013-08-08 10:32:20.707}]
In the From server situation, handleGet
is executed:
void handleGet(HttpRequest request) { HttpResponse res = request.response; (6) print('${request.method}: ${request.uri.path}'), addCorsHeaders(res); (7) res.headers.contentType = new ContentType("application", "json", charset: 'utf-8'),(8) if (jsonList != null) { String jsonString = JSON.encode(jsonList); (9) print('JSON list in GET: ${jsonList}'), res.write(jsonString); (10) } res.close(); (11) }
Here, the response is prepared from line (6)
onwards; line (8)
sets the content type of the server response to the JSON text. In line (9)
, the jsonList
server variable is encoded to a JSON string and written into the response stream in line (10)
, which is then closed in line (11)
. The server prints out the following:
GET: / JSON list in GET: [{title: washing dishes, completed: true, updated: 2013-08-08 15:40:51.999}, {title: walking the dog, completed: true, updated: 2013-08-08 10:30:47.794}, {title: cleaning the kitchen, completed: false, updated: 2013-08-08 15:21:44.626}, {title: buying vegetables, completed: false, updated: 2013-08-08 10:32:20.707}]
The client then prints out:
JSON list from the server: ... same list as above ...
In line (7)
, the addCorsHeaders
method adds the following so-called CORS (Cross Origin Resource Sharing) headers to the response:
void addCorsHeaders(HttpResponse response) { response.headers.add('Access-Control-Allow-Origin', '*, '), response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS'), response.headers.add('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'), }
In order to prevent cross-site scripting attacks, browser vendors have added a same origin policy
to their browsers. If your web page comes from a server at URL domain1, you can only send requests to the same domain1 (remember that a domain consists of a name and port, and not just name—localhost:8080
and localhost:8081
are two different domains). If the server sends CORS headers back in the response, the client can also send requests to other servers. In general, it is not safe to use CORS headers. However, for development purposes, it is useful to allow them so that you can run apps from Dart Editor that uses 3030
by default for its internal server.
But wait! Something is not yet right; the data of a new client overwrites on the server the data from a previous client, so we have to implement some form of data integration:
In handlePost
on the server (bin/server.dart
), we will now call _integrateDataFromClient(jsonList)
, which contains the algorithm for the merging of tasks:
_integrateDataFromClient(List<Map> jsonList) { var clientTasks = new Tasks.fromJson(jsonList); var serverTasks = tasks; var serverTaskList = serverTasks.toList(); for (var serverTask in serverTaskList) { if (!clientTasks.contains(serverTask.title)) { serverTasks.remove(serverTask); } } for (var clientTask in clientTasks) { if (serverTasks.contains(clientTask.title)) { var serverTask = serverTasks.find(clientTask.title); if (serverTask.updated.millisecondsSinceEpoch < clientTask.updated.millisecondsSinceEpoch) { serverTask.completed = clientTask.completed; serverTask.updated = clientTask.updated; } } else { serverTasks.add(clientTask); } } }
This means that the server now has to know about the model (class Task
/Tasks
) to realize that we share the model between the client and server by making it into a library (lib/shared_model.dart
):
library shared_model; import 'dart:convert'; part 'model/model.dart';
By importing this in server.dart
and idb_client.dart
as well:
import 'package:client_server/shared_model.dart';
First, start the server and then two or more clients, for example, the first in Dartium and the second in Chrome or another browser (run as JavaScript) (see doc/use.txt
). Then, juggle a few tasks between them!
As an improvement to the completeTasks
method in Spiral s03, we now have the complete
method, which guarantees that it will wait until all the update tasks are finished by using Future.wait
on a futureList
list of all the following tasks:
Future complete() { var futureList = new List<Future>(); for (var task in tasks) { if (!task.completed) { task.completed = true; task.updated = new DateTime.now(); futureList.add(update(task)); } } return Future.wait(futureList); }
18.223.172.132