크로스플랫폼 채팅 애플리케이션

서버

처음에는 사랑해 마지않는 Rust로 하려 했으나... 비동기 지원이 까다롭고, 채팅 프로토콜을 JSON으로 구성할 생각이여서 Typescript로 결정.

v0.1?

첫 단계에서는 일단 간단하게 메시지를 받고 접속한 클라이언트에게 재전송하는 역할을 수행하는 서버 코드를 작성했다.

한 포트에서 연결을 받으면서 클라이언트가 보내는 첫 메시지에 따라서 해당 연결이 클라이언트의 리시버인지 혹은 센더인지를 파악한다.

일단은 단순한 int형만을 송수신하면서 Sender와 Receiver를 구분하지만 좀 더 개발을 진행하면 첫 수신 객체에 따라서 Sender와 Receiver를 구분하게 만들 수 있을 것이다...

import net = require("net");
import Socket = net.Socket;
import { Buffer } from "buffer";
const EndofTransmissionBlock = 0x17;
class Receiver{
    public constructor(socket:Socket){
        this.socket = socket;
        this.buffer = new Buffer(0);
        this.socket.on("data",(data:Buffer)=> this.PollMessage(data));
    }
    /**
     * PollMessage
     */
    public PollMessage(data: Buffer) {
        let oldBuffer = this.buffer;
        if (oldBuffer.byteLength + data.byteLength > 4096) {
            this.buffer = Buffer.alloc(0, 0);
            return;
        }
        let newBuffer = new Buffer(oldBuffer.byteLength + data.byteLength);
        newBuffer.set(oldBuffer, 0);
        newBuffer.set(data, oldBuffer.byteLength);
        //var s =  data.
        //var s = data.toString("utf-8");
        let lastETBIndex = -1;
        let msgs = Array<string>();
        for (let i = 0; i < newBuffer.byteLength; i++){
            let ch = newBuffer.readUInt8(i);
            if(ch == EndofTransmissionBlock)
            {
                msgs.push(newBuffer.toString("utf8", lastETBIndex + 1, i));
                lastETBIndex = i;
            }
        }
        this.buffer = newBuffer.slice(lastETBIndex);
        let ETB = new Buffer(1);
        ETB.writeUInt8(EndofTransmissionBlock, 0);
        for (let msg of msgs) {
            for (let sender of senders.values()) {
                sender.write(msg);
                sender.write(ETB);
            }
        }
    }
    public buffer:Buffer;
    public socket:Socket;
}
let last_index = 0;
let senders = new Map<number, Socket>();
let receivers = new Map<number, Receiver>();
let server = net.createServer((socket) => {
    socket.on("data", (data: Buffer) => {
        socket.removeAllListeners("data");
        let index: number = data.readInt32LE(0);
        if (index == 0) {
            last_index++;
            let bu = new Buffer(4);
            bu.writeInt32LE(last_index, 0);
            socket.write(bu);
            senders.set(last_index, socket);
            socket.on("close", () => {
                senders.delete(index);
            });
            socket.on("error", () => {
                senders.delete(index);
            });
            return;
        }
        if (senders.has(index) && !receivers.has(index)) {
            socket.write(data);
            let receiver = new Receiver( socket);
            
            receivers.set(index, receiver);
            socket.on("close", () => {
                receivers.delete(index);
            });
            socket.on("error", () => {
                receivers.delete(index);
            });
        }
        else {
            socket.destroy();
        }
    });
});
server.listen(8080);

클라이언트

클라이언트는 역시 사랑해 마지않는 최근에 계속 만지작거리고 있는 Xamarin.Forms (feat. C#)으로 결정. Android, UWP, IOS, WPF, mac os등 리눅스 뺀 메이저한 OS를 원소스로 애플리케이션을 제작할 수 있다.

v0.1?

모든 코드는 보여줄 필요 없이 단순하게 모델만 모여줘도 서버와의 통신을 확인할 수 있다. View는 어차피 Model의 property를 바인딩 한 것뿐이니까.

namespace SimpleChattingClient
{
    public class ChatListModels : INotifyPropertyChanged
    {
        private const string HOST = "localhost";
        private const int PORT = 8080;

        private ObservableCollection<string> chatList;
        private string message;
        private Socket senderSocket;
        private Socket receiveSocket;
        private int uid;
        public event PropertyChangedEventHandler PropertyChanged;
        private Task sendTask = null;
        private Task receiverTask = null;
        public ChatListModels()
        {
            chatList = new ObservableCollection<string>();
            ConnectToServer();
        }
        private async void ConnectToServer()
        {
            receiveSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
            await receiveSocket.ConnectAsync(HOST, PORT);
            var uidBytes = BitConverter.GetBytes(uid);
            int s = 0;
            do
            {
                s += receiveSocket.Send(uidBytes, s, uidBytes.Length - s, SocketFlags.None);
            }
            while (s != uidBytes.Length);
            s = 0;
            do
            {
                s += receiveSocket.Receive(uidBytes, s, uidBytes.Length - s, SocketFlags.None);
            }
            while (s != uidBytes.Length);
            uid = BitConverter.ToInt32(uidBytes, 0);

            senderSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
            await senderSocket.ConnectAsync(HOST, PORT);
            s = 0;
            do
            {
                s += senderSocket.Send(uidBytes, s, uidBytes.Length - s, SocketFlags.None);
            }
            while (s != uidBytes.Length);
            s = 0;
            do
            {
                s += senderSocket.Receive(uidBytes, s, uidBytes.Length - s, SocketFlags.None);
            }
            while (s != uidBytes.Length);
            if (uid != BitConverter.ToInt32(uidBytes, 0))
            {
                throw new Exception("Could not connect to server!");
            }
            else
            {

            }
            receiverTask = Task.Run(() =>
            {
                var stream = new NetworkStream(receiveSocket);
                var chunk = new byte[4096];
                var buffer = new List<byte>();
                int ETB = 0;
                while (true)
                {
                    int readByte = stream.Read(chunk, 0, 4096);
                    if (readByte == 0) continue;
                    buffer.AddRange(chunk.Take(readByte));
                    while (true)
                    {
                        ETB = buffer.FindIndex((byte it) => it == 0x17);
                        if (ETB == -1)
                        {
                            break;
                        }
                        if(ETB != 0)
                        {
                            string text = Encoding.UTF8.GetString(buffer.ToArray(), 0, ETB);
                            chatList.Add(text);
                        }
                     
                        buffer.RemoveRange(0, ETB + 1);
                        
                        //PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ChatList"));
                    }
                }

                //receiveSocket.Receive()
            });
        }
        public async void SendMessageToServer()
        {
            if (message.Trim().Length == 0) return;
            message = message.Trim();
            if (sendTask != null)
            {
                await sendTask;
            }
            sendTask = Task.Run(() =>
            {
                var bytes = Encoding.UTF8.GetBytes(message);
                Message = "";

                int s = 0;
                do
                {
                    s += senderSocket.Send(bytes, s, bytes.Length - s, SocketFlags.None);
                }
                while (s != bytes.Length);
                bytes[0] = 0x17;
                while (senderSocket.Send(bytes, 0, 1, SocketFlags.None) != 1) ;
                sendTask = null;
            });
            //sendTask.Start();
        }
        public Command SendCommand
        {
            get => new Command(() => SendMessageToServer());
        }
        public string Message
        {
            get => message;
            set {

                message = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Message"));
            }

        }
        public ObservableCollection<string> ChatList { get => chatList; }

    }
}