서버
처음에는 사랑해 마지않는 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; }
}
}