Пишем свой tcp-мультиплеерный сервер

в 13:59, , рубрики: .net, .net 4.5, C#, tcp, сервер

В этой серии статей хочу рассказать о том, как создать tcp мультиплеерный сервер. Плюс к этому я хочу сделать его лучше, чем он ей сейчас, и научится лучше программировать — с помощью комментариев.

В этом посте мы изучим, как подключиться к серверу, в следующем я планирую разобраться с тем, как передавать цельные пакеты данных.

Первым делом надо создать решение и 2 проекта в нем. Два проекта — это, собственно, сам сервер, а так же тесты к нему. Первым делом сделаем тесты доверительной библиотекой для сервера для доступа к internal c помощью добавления в AssemblyInfo.cs строчк: [assembly: InternalsVisibleTo(«НАЗВАНИЕ ПРОЕКТА»)]. Так же добавим к проекту с тестами библиотеку NUnit (это лишь мое предпочтение). На этом первоначальные приготовления закончены.

Итак, что бы я хотел получить? Что-то такое:

[Test]
public void TestConnection() {
	var port = 4531;
	
	new ConnectionDispatcher(port).Start();
	
	var tcpClient = new TcpClient();
	tcpClient.Connect("127.0.0.1", port);
	Assert.True(tcpClient.Connected);
}

Вначале делаем этот код компилируемым, добавляем в проект сервера класс ConnectionDispatcher:

internal sealed class ConnectionDispatcher {
	public ConnectionDispatcher(int port) {
		throw new NotImplementedException();
	}

	internal void Start() {
		throw new NotImplementedException();
	}
}

Запускаем тесты. Красная полоска. Отлично. Заставим этот тест выполниться. Прежде всего нам нужно инициализировать «слушателя» на требуемом порту и в методе старт запустить его. Попробуем.

internal sealed class ConnectionDispatcher {
	private TcpListener _listener;
	internal ConnectionDispatcher(int port) {
		_listener = new TcpListener(IPAddress.Any, port);
	}

	internal void Start() {
		_listener.Start();
	}
}

Запускаем и получаем зеленую полоску. Отлично! Можно пойти приготовить кофе.

Итак, теперь я умею соединяться со своим сервером. Что я еще сегодня хочу? Хочу начать обрабатывать свои подключения и для тестирования этого хочу получить один байт со значением 7 от серверного потока, в знак того, что я подключен.

[Test]
public void TestConnection() {
	var port = 4531;
	
	new ConnectionDispatcher(port).Start();
	
	var tcpClient = new TcpClient();
	tcpClient.Connect("127.0.0.1", port);
	Assert.True(tcpClient.Connected);
	
	var receivedBytes = new byte[1];
	tcpClient.GetStream().Read(receivedBytes, 0, receivedBytes.Length);
	Assert.AreEqual(7,receivedBytes[0]);
}

Запускаем. АААА-тест завис и в итоге я получил красную. Больше всего я не люблю зависаний, так что попытаюсь это исправить:

[Test]
public void TestConnection() {
	var port = 4531;

	new ConnectionDispatcher(port).Start();

	var tcpClient = new TcpClient();
	tcpClient.Connect("127.0.0.1", port);
	Assert.True(tcpClient.Connected);

	var receivedBytes = new byte[1];
	var success =
		tcpClient.GetStream()
			.BeginRead(receivedBytes, 0, receivedBytes.Length, null, null)
			.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(100));
	if(!success) Assert.Fail();

	Assert.AreEqual(7, receivedBytes[0]);
}

Итак, с зависанием покончено, попробуем заставить тест выполняться. Для этого примем клиента на сервере:

 internal void Start() {
	_listener.Start();
	var client = _listener.AcceptTcpClient();
	client.GetStream().WriteByte(7);
}
 

АААА-тест завис и в итоге я получил красную. Опять. На этот раз зависание происходит из-за того, что я начинаю принимать клиентов при вызове метода Start, но клиент подключается позже. Нам всего лишь надо процесс принятия клиента вынести в отдельный поток.

internal void Start() {
	_listener.Start();
	 Task.Factory.StartNew(Butler);
}
//У меня правда не получилось выбрать имя получше
private void Butler() {
	while (true) {
		if (_listener.Pending()) {
			var client = _listener.AcceptTcpClient();
			client.GetStream().WriteByte(7);
		}
	}
}
 

Запускаем тест. УРА! Я вновь получил зеленую. Что бы закрепить успех, попробуем подключить 2 клиентов:

[Test]
public void TestConnection() {
	var port = 4531;

	new ConnectionDispatcher(port).Start();
	for (var i = 0; i < 2; i++) {
		var tcpClient = new TcpClient();
		tcpClient.Connect("127.0.0.1", port);
		Assert.True(tcpClient.Connected);

		var receivedBytes = new byte[1];
		var success =
			tcpClient.GetStream()
				.BeginRead(receivedBytes, 0, receivedBytes.Length, null, null)
				.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(100));
		if (!success) Assert.Fail();

		Assert.AreEqual(7, receivedBytes[0]);
	}
}
  

Запускаем тест. УРА! Я вновь получил зеленую. На самом деле немного поспешил и сделал дворецкого так, что он в бесконечном цикле принимает новых клиентов. В этот раз мой большой шаг сработал. Теперь у нас есть собственный сервер, который может рассылать байты направо и налево. Можно отвлечься на перекур.

Настало пора рефакторинга. Хоть и код прекрасно справляется с тестами, я хочу идеальный код. Сейчас больше всего мое внимание привлекает тот факт, что мы обрабатываем подключение клиентов синхронно, но мы же делаем популярную игру и в короткий промежуток времени, наши байты захотят получить многие, но пока подключается 1 клиент, второй будет ждать. К тому же подключение клиента в будущем будет не такое легкое и быстрое. Я понимаю, что решаю задачу, которой еще нет, но в данной ситуации положусь на свое чутье. Посмотрим еще раз на нашего дворецкого. Вот синхронное место «var client = _listener.AcceptTcpClient();», как хорошо, что у TcpListener есть возможность принимать клиентов асинхронно. IOCP.
Попробуем:

//У меня правда не получилось выбрать имя получше
private void Butler() {
	while (true) {
		if (_listener.Pending()) _listener.BeginAcceptTcpClient(AcceptClient, _listener);
	}
}

private void AcceptClient(IAsyncResult ar) {
	var client = _listener.EndAcceptTcpClient(ar);
	client.GetStream().WriteByte(7);
}

Я получил зеленую. Кстати, думаю в тесте нам надо установить тайм аут не магическим числом:

[Test]
public void TestConnection() {
	const int port = 4531;
	const int timeOut = 1000;
	new ConnectionDispatcher(port).Start();

	for (var i = 0; i < 2; i++) {
		var tcpClient = new TcpClient();
		tcpClient.Connect("127.0.0.1", port);
		Assert.True(tcpClient.Connected);

		var receivedBytes = new byte[1];
		var success =
			tcpClient.GetStream()
				.BeginRead(receivedBytes, 0, receivedBytes.Length, null, null)
				.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(timeOut));
		if (!success) Assert.Fail();

		Assert.AreEqual(7, receivedBytes[0]);
	}
}

Теперь мне не нравится, что наш диспетчер занимается не своими обязанностями и посылает байты, но мы с этим разберемся уже в следующий раз. А сейчас у нас есть крутой сервер, умеющий посылать байт. Планы на будущее — это выделить объект клиента и научится посылать и принимать цельные пакеты данных.

P.S. Критика приветствуется.

Автор: Lailore

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js