diff options
author | ryo <ryo@nopwd.lol> | 2025-04-11 20:22:41 +0000 |
---|---|---|
committer | ryo <ryo@nopwd.lol> | 2025-04-11 20:22:41 +0000 |
commit | 8e12f4f68cc907668a0ad91b854a80f21b921071 (patch) | |
tree | f9f3dffba3fa0284fbef050367cdc6e4ffbc0f42 |
-rw-r--r-- | README.md | 14 | ||||
-rw-r--r-- | demo-socket.png | bin | 0 -> 21937 bytes | |||
-rw-r--r-- | main.dart | 285 | ||||
-rw-r--r-- | pubspec.yaml | 21 | ||||
-rw-r--r-- | server.go | 89 |
5 files changed, 409 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..55438d2 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Socket Demo - server and client +Simple chat server using TCP socket. It's just a demo, there are so many features +that I will add later. + +- Server: Goalng +- Client: Flutter + +## Notes +- Run server first, I haven't implemented reconnect feature to the client yet. +- Default username is ip address, change it by __login__ command. +- Commands are just messages that starts with `/` like `/login`. +- There is no register but just login, to do it: `/login <your name>`. + + diff --git a/demo-socket.png b/demo-socket.png Binary files differnew file mode 100644 index 0000000..d6c5ed6 --- /dev/null +++ b/demo-socket.png diff --git a/main.dart b/main.dart new file mode 100644 index 0000000..266b670 --- /dev/null +++ b/main.dart @@ -0,0 +1,285 @@ +import 'package:flutter/material.dart'; +import 'dart:io'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + brightness: Brightness.light, + ), + darkTheme: ThemeData( + brightness: Brightness.dark, + ), + themeMode: ThemeMode.dark, + home: const MyHomePage(title: 'Socket Demo'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State<MyHomePage> createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State<MyHomePage> { + late Socket _socket; + final TextEditingController _inputController = TextEditingController(); + final List<String> _msgs = []; + String address = ""; + + @override + void initState() { + super.initState(); + _connect(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.purple, + title: Text(widget.title), + ), + body: Column(children: [ + Expanded( + child: ListView.builder( + padding: EdgeInsets.all(10.0), + itemCount: _msgs.length, + itemBuilder: (context, index) { + var msg = _msgs[index]; + String user = ""; + String data = ""; + int i; + for (i = 0; i < msg.length; i++) { + if (msg[i] == '=') { + i++; + break; + } + user += msg[i]; + } + for (; i < msg.length; i++) { + data += msg[i]; + } + + if (user == address) { + return BubbleSend(message: data, user: user); + } else { + return BubbleReceive(message: data, user: user); + } + }, + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _inputController, + onSubmitted: (value) { + _send(); + }, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Enter message', + ), + ), + ), + IconButton( + icon: Icon(Icons.send), + onPressed: () { + _send(); + }, + ), + ], + ), + ), + ]), + ); + } + + void _connect() async { + try { + _socket = await Socket.connect("127.0.0.1", 7654); + address = "${_socket.address.address}:${_socket.port}"; + print("my address: $address"); + toast("Connected to 127.0.0.1:7654", 0); + + _socket.listen( + (data) { + final message = String.fromCharCodes(data).trim(); + print('received: $message'); + if (message[0] == '!') { + String name = ""; + for (int i = 1; i < message.length; i++) { + name += message[i]; + } + address = name; + } else { + setState(() { + _msgs.addAll(message.split('\n')); + }); + // print("_msgs: $_msgs"); + } + }, + onDone: () { + print('Server disconnected.'); + toast('Server disconnected', 1); + _socket.destroy(); + }, + onError: (error) { + print('Error: $error'); + toast('Error: $error', 1); + _socket.destroy(); + }, + ); + } catch (e) { + print('Error: $e'); + toast('An unexpected error occurred: $e', 1); + } + } + + void _send() { + final value = _inputController.text; + if (value.isNotEmpty) { + _socket.write('$value\n'); + _inputController.clear(); + } + } + + void toast(String msg, int type) { + Color bg = Color(0x7aa2f7ff); + Color fg = Colors.black; + switch (type) { + case 0: + bg = Colors.green; + break; + case 1: + bg = Colors.red; + break; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(msg, style: TextStyle(color: fg)), + backgroundColor: bg, + )); + } +} + +class BubbleReceive extends StatelessWidget { + final String message; + final String user; + const BubbleReceive({super.key, required this.message, required this.user}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible( + child: Container( + padding: EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.blueGrey, + borderRadius: BorderRadius.circular(15), + ), + child: Text( + message, + style: const TextStyle(color: Colors.white, fontSize: 15), + ), + ), + ), + SizedBox( + width: 20, + height: 20, + child: CustomPaint(painter: Triangle(false, Colors.blueGrey)), + ), + ], + ), + Text(user), + SizedBox(height: 10), + ]); + } +} + +class BubbleSend extends StatelessWidget { + final String message; + final String user; + const BubbleSend({super.key, required this.message, required this.user}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox( + width: 20, + height: 20, + child: CustomPaint(painter: Triangle(true, Colors.indigo)), + ), + Container( + padding: EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.indigo, + borderRadius: BorderRadius.circular(15), + ), + child: Text( + message, + style: const TextStyle(color: Colors.white, fontSize: 15), + ), + ), + ], + ), + Text(user), + SizedBox(height: 10), + ]); + } +} + +class Triangle extends CustomPainter { + final Color backgroundColor; + final bool send; + Triangle(this.send, this.backgroundColor); + + @override + void paint(Canvas canvas, Size size) { + var paint = Paint()..color = backgroundColor; + + var path = Path(); + if (send) { + path.moveTo(20, 0); + path.lineTo(5, 15); + path.lineTo(35, 15); + } else { + path.lineTo(15, 15); + path.lineTo(-15, 15); + } + path.close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..9565813 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,21 @@ +name: chat +description: "Socket Demo" +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ^3.6.0 + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true diff --git a/server.go b/server.go new file mode 100644 index 0000000..56cd582 --- /dev/null +++ b/server.go @@ -0,0 +1,89 @@ +package main + +import ( + "bufio" + "fmt" + "net" + "strings" +) + +var ( + clients = make(map[net.Conn]string) + chats [20]string + chats_i int +) + +func broadcast(message string) { + for conn, _ := range clients { + fmt.Fprint(conn, message) + } +} + +func handleCommand(conn net.Conn, message string) { + parts := strings.Split(message, " ") + command := parts[0] + fmt.Println("command >>", command) + switch command { + case "login": + name := parts[1] + fmt.Printf("%v now is %v\n", clients[conn], name) + clients[conn] = name + fmt.Fprintf(conn, "!%v\n", name) + default: + fmt.Println("Command not found") + } +} + +func handleConnection(conn net.Conn) { + defer conn.Close() + + clients[conn] = conn.RemoteAddr().String() + fmt.Println("Client connected: ", clients[conn]) + fmt.Fprintln(conn, "127.0.0.1:7654=Hello everyone from server") + for i := 0; i < chats_i; i++ { + fmt.Fprint(conn, chats[i]) + } + + for { + message, error := bufio.NewReader(conn).ReadString('\n') + if error != nil { + break + } + if message[0] == '/' { + handleCommand(conn, message[1:len(message)-1]) + } else { + chats[chats_i] = clients[conn] + "=" + message + fmt.Print(">> ", chats[chats_i]) + broadcast(chats[chats_i]) + chats_i = chats_i + 1 + if chats_i >= len(chats) { + chats_i = 0 + } + } + } + + fmt.Println("Client disconnected:", clients[conn]) + delete(clients, conn) +} + +func main() { + listener, err := net.Listen("tcp", "0.0.0.0:7654") + if err != nil { + fmt.Println("Error starting server:", err) + return + } + defer listener.Close() + + fmt.Println("Chat server started on 0.0.0.0:7654") + + chats_i = 0 + for { + conn, err := listener.Accept() + if err != nil { + fmt.Println("Error accepting connection:", err) + continue + } + + go handleConnection(conn) + } +} |