В этой серии статей хочу рассказать о том, как создать 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