364 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Haxe
		
	
	
	
	
	
			
		
		
	
	
			364 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Haxe
		
	
	
	
	
	
| /*
 | |
|  * Copyright (C)2005-2019 Haxe Foundation
 | |
|  *
 | |
|  * Permission is hereby granted, free of charge, to any person obtaining a
 | |
|  * copy of this software and associated documentation files (the "Software"),
 | |
|  * to deal in the Software without restriction, including without limitation
 | |
|  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 | |
|  * and/or sell copies of the Software, and to permit persons to whom the
 | |
|  * Software is furnished to do so, subject to the following conditions:
 | |
|  *
 | |
|  * The above copyright notice and this permission notice shall be included in
 | |
|  * all copies or substantial portions of the Software.
 | |
|  *
 | |
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | |
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | |
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | |
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | |
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 | |
|  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 | |
|  * DEALINGS IN THE SOFTWARE.
 | |
|  */
 | |
| 
 | |
| package cpp.net;
 | |
| 
 | |
| import cpp.vm.Thread;
 | |
| import cpp.net.Poll;
 | |
| import cpp.vm.Lock;
 | |
| 
 | |
| private typedef ThreadInfos = {
 | |
| 	var id:Int;
 | |
| 	var t:Thread;
 | |
| 	var p:Poll;
 | |
| 	var socks:Array<sys.net.Socket>;
 | |
| }
 | |
| 
 | |
| private typedef ClientInfos<Client> = {
 | |
| 	var client:Client;
 | |
| 	var sock:sys.net.Socket;
 | |
| 	var thread:ThreadInfos;
 | |
| 	var buf:haxe.io.Bytes;
 | |
| 	var bufpos:Int;
 | |
| }
 | |
| 
 | |
| /**
 | |
| 	The ThreadServer can be used to easily create a multithreaded server where each thread polls multiple connections.
 | |
| 	To use it, at a minimum you must override or rebind clientConnected, readClientMessage, and clientMessage and you must define your Client and Message.
 | |
| **/
 | |
| class ThreadServer<Client, Message> {
 | |
| 	var threads:Array<ThreadInfos>;
 | |
| 	var sock:sys.net.Socket;
 | |
| 	var worker:Thread;
 | |
| 	var timer:Thread;
 | |
| 
 | |
| 	/**
 | |
| 		Number of total connections the server will accept.
 | |
| 	**/
 | |
| 	public var listen:Int;
 | |
| 
 | |
| 	/**
 | |
| 		Number of server threads.
 | |
| 	**/
 | |
| 	public var nthreads:Int;
 | |
| 
 | |
| 	/**
 | |
| 		Polling timeout.
 | |
| 	**/
 | |
| 	public var connectLag:Float;
 | |
| 
 | |
| 	/**
 | |
| 		Stream to send error messages.
 | |
| 	**/
 | |
| 	public var errorOutput:haxe.io.Output;
 | |
| 
 | |
| 	/**
 | |
| 		Space allocated to buffers when they are created.
 | |
| 	**/
 | |
| 	public var initialBufferSize:Int;
 | |
| 
 | |
| 	/**
 | |
| 		Maximum size of buffered data read from a socket. An exception is thrown if the buffer exceeds this value.
 | |
| 	**/
 | |
| 	public var maxBufferSize:Int;
 | |
| 
 | |
| 	/**
 | |
| 		Minimum message size.
 | |
| 	**/
 | |
| 	public var messageHeaderSize:Int;
 | |
| 
 | |
| 	/**
 | |
| 		Time between calls to update.
 | |
| 	**/
 | |
| 	public var updateTime:Float;
 | |
| 
 | |
| 	/**
 | |
| 		The most sockets a thread will handle.
 | |
| 	**/
 | |
| 	public var maxSockPerThread:Int;
 | |
| 
 | |
| 	/**
 | |
| 		Creates a ThreadServer.
 | |
| 	**/
 | |
| 	public function new() {
 | |
| 		threads = new Array();
 | |
| 		nthreads = if (Sys.systemName() == "Windows") 150 else 10;
 | |
| 		messageHeaderSize = 1;
 | |
| 		listen = 10;
 | |
| 		connectLag = 0.5;
 | |
| 		errorOutput = Sys.stderr();
 | |
| 		initialBufferSize = (1 << 10);
 | |
| 		maxBufferSize = (1 << 16);
 | |
| 		maxSockPerThread = 64;
 | |
| 		updateTime = 1;
 | |
| 	}
 | |
| 
 | |
| 	function runThread(t) {
 | |
| 		while (true) {
 | |
| 			try {
 | |
| 				loopThread(t);
 | |
| 			} catch (e:Dynamic) {
 | |
| 				logError(e);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	function readClientData(c:ClientInfos<Client>) {
 | |
| 		var available = c.buf.length - c.bufpos;
 | |
| 		if (available == 0) {
 | |
| 			var newsize = c.buf.length * 2;
 | |
| 			if (newsize > maxBufferSize) {
 | |
| 				newsize = maxBufferSize;
 | |
| 				if (c.buf.length == maxBufferSize)
 | |
| 					throw "Max buffer size reached";
 | |
| 			}
 | |
| 			var newbuf = haxe.io.Bytes.alloc(newsize);
 | |
| 			newbuf.blit(0, c.buf, 0, c.bufpos);
 | |
| 			c.buf = newbuf;
 | |
| 			available = newsize - c.bufpos;
 | |
| 		}
 | |
| 		var bytes = c.sock.input.readBytes(c.buf, c.bufpos, available);
 | |
| 		var pos = 0;
 | |
| 		var len = c.bufpos + bytes;
 | |
| 		while (len >= messageHeaderSize) {
 | |
| 			var m = readClientMessage(c.client, c.buf, pos, len);
 | |
| 			if (m == null)
 | |
| 				break;
 | |
| 			pos += m.bytes;
 | |
| 			len -= m.bytes;
 | |
| 			work(clientMessage.bind(c.client, m.msg));
 | |
| 		}
 | |
| 		if (pos > 0)
 | |
| 			c.buf.blit(0, c.buf, pos, len);
 | |
| 		c.bufpos = len;
 | |
| 	}
 | |
| 
 | |
| 	function loopThread(t:ThreadInfos) {
 | |
| 		if (t.socks.length > 0)
 | |
| 			for (s in t.p.poll(t.socks, connectLag)) {
 | |
| 				var infos:ClientInfos<Client> = s.custom;
 | |
| 				try {
 | |
| 					readClientData(infos);
 | |
| 				} catch (e:Dynamic) {
 | |
| 					t.socks.remove(s);
 | |
| 					if (!Std.isOfType(e, haxe.io.Eof) && !Std.isOfType(e, haxe.io.Error))
 | |
| 						logError(e);
 | |
| 					work(doClientDisconnected.bind(s, infos.client));
 | |
| 				}
 | |
| 			}
 | |
| 		while (true) {
 | |
| 			var m:{s:sys.net.Socket, cnx:Bool} = Thread.readMessage(t.socks.length == 0);
 | |
| 			if (m == null)
 | |
| 				break;
 | |
| 			if (m.cnx)
 | |
| 				t.socks.push(m.s);
 | |
| 			else if (t.socks.remove(m.s)) {
 | |
| 				var infos:ClientInfos<Client> = m.s.custom;
 | |
| 				work(doClientDisconnected.bind(m.s, infos.client));
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	function doClientDisconnected(s:sys.net.Socket, c) {
 | |
| 		try
 | |
| 			s.close()
 | |
| 		catch (e:Dynamic) {};
 | |
| 		clientDisconnected(c);
 | |
| 	}
 | |
| 
 | |
| 	function runWorker() {
 | |
| 		while (true) {
 | |
| 			var f = Thread.readMessage(true);
 | |
| 			try {
 | |
| 				f();
 | |
| 			} catch (e:Dynamic) {
 | |
| 				logError(e);
 | |
| 			}
 | |
| 			try {
 | |
| 				afterEvent();
 | |
| 			} catch (e:Dynamic) {
 | |
| 				logError(e);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 		Internally used to delegate something to the worker thread.
 | |
| 	**/
 | |
| 	public function work(f:Void->Void) {
 | |
| 		worker.sendMessage(f);
 | |
| 	}
 | |
| 
 | |
| 	function logError(e:Dynamic) {
 | |
| 		var stack = haxe.CallStack.exceptionStack();
 | |
| 		if (Thread.current() == worker)
 | |
| 			onError(e, stack);
 | |
| 		else
 | |
| 			work(onError.bind(e, stack));
 | |
| 	}
 | |
| 
 | |
| 	function addClient(sock:sys.net.Socket) {
 | |
| 		var start = Std.random(nthreads);
 | |
| 		for (i in 0...nthreads) {
 | |
| 			var t = threads[(start + i) % nthreads];
 | |
| 			if (t.socks.length < maxSockPerThread) {
 | |
| 				var infos:ClientInfos<Client> = {
 | |
| 					thread: t,
 | |
| 					client: clientConnected(sock),
 | |
| 					sock: sock,
 | |
| 					buf: haxe.io.Bytes.alloc(initialBufferSize),
 | |
| 					bufpos: 0,
 | |
| 				};
 | |
| 				sock.custom = infos;
 | |
| 				infos.thread.t.sendMessage({s: sock, cnx: true});
 | |
| 				return;
 | |
| 			}
 | |
| 		}
 | |
| 		refuseClient(sock);
 | |
| 	}
 | |
| 
 | |
| 	function refuseClient(sock:sys.net.Socket) {
 | |
| 		// we have reached maximum number of active clients
 | |
| 		sock.close();
 | |
| 	}
 | |
| 
 | |
| 	function runTimer() {
 | |
| 		var l = new Lock();
 | |
| 		while (true) {
 | |
| 			l.wait(updateTime);
 | |
| 			work(update);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	function init() {
 | |
| 		worker = Thread.create(runWorker);
 | |
| 		timer = Thread.create(runTimer);
 | |
| 		for (i in 0...nthreads) {
 | |
| 			var t = {
 | |
| 				id: i,
 | |
| 				t: null,
 | |
| 				socks: new Array(),
 | |
| 				p: new Poll(maxSockPerThread),
 | |
| 			};
 | |
| 			threads.push(t);
 | |
| 			t.t = Thread.create(runThread.bind(t));
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 		Called when the server gets a new connection.
 | |
| 	**/
 | |
| 	public function addSocket(s:sys.net.Socket) {
 | |
| 		s.setBlocking(false);
 | |
| 		work(addClient.bind(s));
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 		Start the server at the specified host and port.
 | |
| 	**/
 | |
| 	public function run(host, port) {
 | |
| 		sock = new sys.net.Socket();
 | |
| 		sock.bind(new sys.net.Host(host), port);
 | |
| 		sock.listen(listen);
 | |
| 		init();
 | |
| 		while (true) {
 | |
| 			try {
 | |
| 				addSocket(sock.accept());
 | |
| 			} catch (e:Dynamic) {
 | |
| 				logError(e);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 		Send data to a client.
 | |
| 	**/
 | |
| 	public function sendData(s:sys.net.Socket, data:String) {
 | |
| 		try {
 | |
| 			s.write(data);
 | |
| 		} catch (e:Dynamic) {
 | |
| 			stopClient(s);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 		Shutdown a client's connection and remove them from the server.
 | |
| 	**/
 | |
| 	public function stopClient(s:sys.net.Socket) {
 | |
| 		var infos:ClientInfos<Client> = s.custom;
 | |
| 		try
 | |
| 			s.shutdown(true, true)
 | |
| 		catch (e:Dynamic) {};
 | |
| 		infos.thread.t.sendMessage({s: s, cnx: false});
 | |
| 	}
 | |
| 
 | |
| 	// --- CUSTOMIZABLE API ---
 | |
| 
 | |
| 	/**
 | |
| 		Called when an error has ocurred.
 | |
| 	**/
 | |
| 	public dynamic function onError(e:Dynamic, stack) {
 | |
| 		var estr = try Std.string(e) catch (e2:Dynamic) "???" + try "[" + Std.string(e2) + "]" catch (e:Dynamic) "";
 | |
| 		errorOutput.writeString(estr + "\n" + haxe.CallStack.toString(stack));
 | |
| 		errorOutput.flush();
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 		Called when a client connects. Returns a client object.
 | |
| 	**/
 | |
| 	public dynamic function clientConnected(s:sys.net.Socket):Client {
 | |
| 		return null;
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 		Called when a client disconnects or an error forces the connection to close.
 | |
| 	**/
 | |
| 	public dynamic function clientDisconnected(c:Client) {}
 | |
| 
 | |
| 	/**
 | |
| 		Called when data has been read from a socket. This method should try to extract a message from the buffer.
 | |
| 		The available data resides in buf, starts at pos, and is len bytes wide. Return the new message and the number of bytes read from the buffer.
 | |
| 		If no message could be read, return null.
 | |
| 	**/
 | |
| 	public dynamic function readClientMessage(c:Client, buf:haxe.io.Bytes, pos:Int, len:Int):{msg:Message, bytes:Int} {
 | |
| 		return {
 | |
| 			msg: null,
 | |
| 			bytes: len,
 | |
| 		};
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 		Called when a message has been recieved. Message handling code should go here.
 | |
| 	**/
 | |
| 	public dynamic function clientMessage(c:Client, msg:Message) {}
 | |
| 
 | |
| 	/**
 | |
| 		This method is called periodically. It can be used to do server maintenance.
 | |
| 	**/
 | |
| 	public dynamic function update() {}
 | |
| 
 | |
| 	/**
 | |
| 		Called after a client connects, disconnects, a message is received, or an update is performed.
 | |
| 	**/
 | |
| 	public dynamic function afterEvent() {}
 | |
| }
 |