做爲一個Android開發,基本沒怎麼接觸後臺開發的東西,對這方面也有點興趣,一直都想寫套接口實現下簡單的後端服務玩一玩。 Flutter也學習了快一年了,加上以前看了下閒魚的一篇文章Flutter & Dart三端一體化開發,興趣就來了,有興趣就有學習熱情。因而將Dart的HttpServer學習了一下,實現了一個簡單的聊天室應用。html
作這個應用還有其餘的目的:linux
Github源碼,包括了客戶端和服務端的代碼。android
clone項目後,能夠在本地運行個人客戶端代碼。基本上是一套代碼,目前只適配了app和desktop平臺。(web遇到點問題,因此還沒弄好)git
因爲時間關係,也只是作了下一些最基本的功能,後面有空再繼續完善。github
這是生成的接口文檔地址:web
ApplicationChannel(應用通道),每一個aqueduct應用程序會根據isolate數目去啓動相應數量的ApplicationChannel(一個isolate會建立一個ApplicationChannel)。sql
不一樣的HTTP請求,會根據Router配置的路徑,由不一樣的Controller進行處理。每一個裏面都有相應的邏輯去處理HTTP請求。數據庫
能夠連接多個controller處理,造成子通道。好比實現一個獲取好友列表的接口,很明顯,前提是咱們須要在請求接口的時候帶上用戶信息(好比token)。這樣的話,就能夠考慮一個Authorizer controller,用來驗證請求的受權憑據是否正確。再加一個FriendController來獲取好友列表數據做爲response。json
好比下面的代碼,定義了註冊接口和登陸接口的路由。ubuntu
router
.route("/register")
.link(() => RegisterController(authServer, context));
router.route("/login").link(() => LoginController(context));
複製代碼
針對不一樣的接口,定義Controller進行相應的處理。下面的登陸接口的相關代碼。
class LoginController extends ResourceController {
final ManagedContext context;
LoginController(this.context);
@Operation.post()
Future<Response> login(@Bind.body() User user) async {
String msg = "登陸異常";
//查詢數據庫是否存在這個用戶
var query = Query<User>(context)
..where((u) => u.username).equalTo(user.username);
User result = await query.fetchOne();
if (result == null) {
msg = "用戶不存在";
} else {
//經過auth/token獲取token。登陸成功的話,返回token
var clientId = "com.donggua.chat";
var clientSecret = "dongguasecret";
var body =
"username=${user.username}&password=${user.password}&grant_type=password";
var clientCredentials =
Base64Encoder().convert("$clientId:$clientSecret".codeUnits);
res.Response response =
await http.post("http://127.0.0.1:8888/auth/token",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Basic $clientCredentials"
},
body: body);
if (response.statusCode == 200) {
var map = json.decode(response.body);
return Response.ok(
BaseResult(
code: 1,
msg: "登陸成功",
data: {
'userId': result.id,
'access_token': map['access_token'],
'userName': result.username
},
),
);
}
}
return Response.ok(
BaseResult(
code: 1,
msg: msg,
),
);
}
}
複製代碼
//跟服務器創建鏈接
router
.route("/connect")
.link(() => Authorizer.bearer(authServer))
.linkFunction((request) async {
//鏈接的用戶id
int userId = request.authorization.ownerID;
var socket = await WebSocketTransformer.upgrade(request.raw);
print("userId:$userId的用戶跟服務器創建鏈接");
socket.listen((event) {
print("server listen:${event}");
handleEvent(event, fromUserId: userId);
messageHub.add(
{
"event": "websocket_broadcast",
"message": event,
'fromUserId': userId,
},
);
}, onDone: () {
//socket鏈接斷了的話,移除鏈接
connections.remove(userId);
});
//保存鏈接
connections[userId] = socket;
print("當前鏈接用戶有${connections.length}個");
connections.keys.forEach((userId) {
print("userId:$userId");
});
return null;
});
複製代碼
項目目錄下有一個config.yaml文件,用來實現一些信息的配置,好比數據庫方面的配置。
database:
host: localhost
port: 5432
username: donggua
password: password
databaseName: database_chat
複製代碼
在項目中初始化數據庫。在prepare()方法中,進行數據庫的鏈接,並獲取到數據庫的上下文ManagedContext對象。將ManagedContext保存到一個context的成員變量中,而後能夠傳給須要數據庫操做的controller的構造函數,這樣的話,咱們就能夠在controller裏面進行一些數據庫方面的操做。
@override
Future prepare() async {
final config = CustomConfig(options.configurationFilePath);
final dateModel = ManagedDataModel.fromCurrentMirrorSystem();
final persistentStore = PostgreSQLPersistentStore.fromConnectionInfo(
config.database.username,
config.database.password,
config.database.host,
config.database.port,
config.database.databaseName);
context = ManagedContext(dateModel, persistentStore);
複製代碼
在服務器上面安裝Dart sdk,這裏的服務器建議是ubuntu,能夠直接安裝官網的Dart SDK。若是是centOS的話,須要本身下載dart sdk源碼並進行編譯構建,好麻煩,並且可能還會遇到其餘問題。(因此我最後重裝系統,搞成ubuntu系統了)
將本地的服務器代碼,放置到服務器上面。用到兩個工具,SecureCRT和FileZilla,SecureCRT用來搞遠程登陸,FileZilla用來搞文件傳輸。具體使用百度一下。
在服務器上面安裝dart sdk和aqueduct框架
pub global activate aqueduct
複製代碼
這塊具體也能夠百度一下,這裏就不細說了。建立配置的信息,要和咱們的服務端項目中的配置信息保持一致就行。
aqueduct serve
複製代碼
通常狀況下,當咱們關閉遠程窗口的話,項目就跟着退出運行了。因此可使用Screen來讓咱們在關閉ssh鏈接的狀況下,讓程序繼續在後臺運行。screen命令能夠實現當前窗口與任務分離,咱們即便離線了,服務器仍在後臺運行任務。當咱們從新登陸服務器,能夠讀取窗口線程,從新鏈接任務窗口。
推薦一篇文章,瞭解下什麼是Screen。linux 技巧:使用 screen 管理你的遠程會話。
客戶端實現,Flutter。客戶端這邊的實現比較簡單,爲了快點體驗出三端一體化的快感,用了一些第三方庫加快節奏。UI的話就是一個登陸註冊頁面,再加上一個聊天列表和聊天窗口頁面。
只是作簡單Demo,因此總體的代碼架構比較簡單,後期再優化下。
UI的代碼就不展現了,無非就是兩個文本框加個登陸按鈕。 看一下以前在前面的項目中,LoginController定義好的登陸接口返回的結構:
//登陸成功
{
"code": 1,
"msg": 「登陸成功」,
"data":{
"userId":"12345",
「access_token」:"abcdefg",
"userName":"donggua"
}
}
//登陸失敗
{
"code":1,
"msg":"登陸異常:具體緣由"
}
複製代碼
點擊按鈕,使用Dio調用以前定義好的後端接口。
void login() async {
Dio dio = Dio(BaseOptions(baseUrl: GetIt.instance<AppConfig>().apiHost));
Response<Map<String, dynamic>> response =
await dio.post<Map<String, dynamic>>(
"/login",
data: {
'username': username_controller.text.toString(),
'password': password_controller.text.toString(),
},
);
print("登陸結果:$response");
if (response != null &&
response.data != null &&
response.data['code'] == 1 &&
response.data['data']['access_token'] != null) {
//登陸成功
String token = response.data['data']['access_token'];
int fromUserId = response.data['data']['userId'];
String userName = response.data['data']['userName'];
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return ChatListPage(
token: token,
fromUserId: fromUserId,
userName: userName,
);
}));
}
}
複製代碼
登陸成功,進入到聊天列表頁面。
1.請求聊天列表接口/chat_list,獲取聊天列表並展現。(後臺定義接口類ChatListController,注意客戶端接口請求是要帶上token的,由於服務端會作token驗證。若token無效,則返回401錯誤碼)。
getChatList() async {
Dio dio = Dio(BaseOptions(
baseUrl: GetIt.instance<AppConfig>().apiHost,
headers: {'Authorization': 'Bearer ${widget.token}'}));
Response<Map<String, dynamic>> response =
await dio.get<Map<String, dynamic>>(
"/chat_list",
);
if (response != null &&
response.data != null &&
response.data['code'] == 1) {
List list = response.data['data'];
list?.forEach((json) {
userList.add(User.fromJson(json));
});
setState(() {});
}
}
複製代碼
2.使用SocketManager建立WebSocket鏈接,使客戶端和服務器之間能夠進行通訊。
void initState(){
socketManager.connectWithServer(widget.token).then((bool) {
if (bool) {
showToast("鏈接服務器成功");
} else {
showToast("鏈接服務器失敗");
}
});
}
複製代碼
1.打開聊天詳情頁面,獲取歷史聊天記錄(App這邊暫時沒作數據保存,因此數據全是在後端的數據庫中)。這裏就不展現代碼了,跟前面的同樣,請求接口,獲取數據後進行展現。有一點就是要根據是不是當前用戶,消息item的展現會有所區別。
2.在文本框中輸入內容,使用socket進行發送消息。
sendMessage() async {
//發送消息
Map<String, dynamic> data = {
'toUserId': widget.toUser.id,
'msg_content': inputController.text.toString(),
'msg_type': 1,
};
GetIt.instance<SocketManager>().sendMessage(data);
//清空editText
inputController.clear();
debugPrint("向服務器發送消息:$data");
}
複製代碼
3.監聽服務器的消息。當接收到服務端的消息後,往ListView的數據源中添加一條消息。
void initState(){
listener = (Map<String, dynamic> json) {
if (mounted) {
setState(() {
print("messageList增長一條消息");
Message newMessage = Message.fromJson(json);
//消息是本身發的,或者是別人要發給本身的,才進行展現
if (newMessage.fromUserId == widget.fromUserId ||
newMessage.toUserId == widget.fromUserId) {
messageList.add(newMessage);
}
});
Future.delayed(Duration(milliseconds: 50), () {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
}
};
//添加監聽
GetIt.instance<SocketManager>().addListener(listener);
}
複製代碼
這裏介紹下以前寫過的一篇文章Flutter之支持不一樣的屏幕尺寸和方向。
這裏的場景是,在App裏面就顯示一個聊天列表頁面,這個頁面是充滿整個屏幕的,點擊item纔會進入一個新的聊天詳情頁面。可是在桌面端或者平板,這種大尺寸的屏幕上,能夠在左側顯示聊天列表,右側顯示聊天詳情,合理地使用屏幕空間。總體的思路是相似Android的Fragment。咱們須要作的就是定義兩個Widget,一個用於顯示主列表,一個用於顯示詳細視圖。實際上,這些就是相似的fragments。
咱們只須要檢查設備是否具備足夠的寬度來處理列表視圖和詳細視圖。若是是,咱們在同一屏幕上顯示兩個widget。若是設備沒有足夠的寬度來包含兩個界面,那咱們只須要在屏幕中展現主列表,點擊列表項後導航到獨立的屏幕來顯示詳細視圖。