Программирование / Заочники / 2 семестр / Методичка_ч3_Урок4
.pdf
Урок 4: Сетевое взаимодействие.
Основная цель урока.
1.Научиться создавать программы, обменивающиеся информацией по сети.
2.Познакомиться с созданием новых потоков (Threads).
3.Познакомиться с анонимными функциями и лямбда-выражениями.
Краткая справка.
Организация сетевого взаимодействия.
Одним из способов организации сетевого взаимодействия является использование классов TcpListener и TcpClient (используется протокол TCP для передачи информации). Система, использующая эти классы должна иметь клиент-
серверную архитектуру, т.е. одна программа должна выполнять роль сервера,
другая – роль клиента. В случае, если функционал клиента и сервера во многом похож можно обойтись одной универсальной программой, которая может быть как сервером, так и клиентом.
Экземпляр класса TcpListener может ожидать и принимать входящие подключения. Для запуска сервера необходимо указать порт (от 0 до 65535),
который будет прослушиваться. Для ожидания подключения нового клиента используется метод AcceptTcpClient. При подключении нового клиента метод
AcceptTcpClient возвращает экземпляр класса TcpClient, через который можно обмениваться информацией с удалённым клиентом.
using System.Net;
using System.Net.Sockets;
//ожидаем подключение с любого адреса на порт 6542 IPEndPoint ipPoint = new IPEndPoint(IPAddress.Any, 6542); TcpListener Server = new TcpListener(ipPoint); //запускаем сервер
Server.Start();
//ожидаем подключения клиента (блокирующая операция) TcpClient RemoteClient = Server.AcceptTcpClient();
Для подключения клиента к серверу необходимо указать IP-адерс сервера и
порт, который прослушивается сервером.
using System.Net.Sockets; TcpClient Client = new TcpClient();
//подключаемся по адресу 192.168.3.12 //на порт 6542 (это блокирующая операция) Client.Connect("192.168.3.12", 6542);
Для приёма\передачи информации используется экземпляры класса TcpClient (т.е. Client и RemoteClient, см. выше). Информация передаётся в виде последовательности байтов (байтового массива), поэтому если планируется передавать строки, их необходимо преобразовать в байтовый массив с помощью класса Encoding.
Передача (запись) информации для сервера (или клиента).
string txt = "Привет!!!!!"; //преобразуем строку в байтовый массив
byte[] data = Encoding.Default.GetBytes(txt); //отправляем на сервер Client.GetStream().Write(data,0,data.Length);
Приём (чтение) информации для сервера (или клиента).
//создаём буфер куда будет помещаться принятая информация byte[] buffer = new byte[1024];
//ожидаем новой информации (блокирующая операция) Client.GetStream().Read(buffer, 0, 1024); //преобразуем в текстовый формат (если возможно) string txt = Encoding.Default.GetString(buffer);
Лямбда-выражения.
Лямбда-выражение — это анонимная функция, которая содержит выражения и операторы. Лямбда-выражения в некоторых случаях позволяют существенно упростить код и сделать его максимально наглядным.
Во всех лямбда-выражениях используется лямбда-оператор =>, который читается как « переходит в». Левая часть лямбда-оператора определяет параметры ввода (если таковые имеются), а правая часть содержит выражение или блок оператора.
Пример 1. Сортировка массива без лямбда выражений.
private int ПоВозрастанию(int x, int y)
{
return x - y;
}
private int ПоУбыванию(int x, int y)
{
return y-x;
}
public Func()
{
int[] arr = {10, 3, 56, 3, 44, 90, 76}; Array.Sort(arr, ПоВозрастанию); Array.Sort(arr, ПоУбыванию);
}
Пример 2. Сортировка массива, используя лямбда выражения.
public fMain()
{
int[] arr = {10, 3, 56, 3, 44, 90, 76}; Array.Sort(arr,(x,y) => x - y); //По возрастанию Array.Sort(arr, (x, y) => y - x);
}
Во втором примере мы не создавали дополнительные методы, которые использовали бы всего один раз, вместо этого написали лямбда-выражение,
(x,y) => x – y
которое представляет собой ту же функцию с двумя аргументами (имена x и y могут быть любыми). После знака “=>” идёт тело функции. Если функция сложная, то тело функции должно заключаться в фигурные скобки и тогда уже возвращать значение нужно с помощью оператора return. Пример:
Array.Sort(arr, (x, y) =>
{
return x - y; }); //По убыванию
Чуть более сложный пример – есть массив, в который занесены оценки ученика,
необходимо посчитать итоговый балл по следующему принципу:
–каждая 5-ка даёт 2 балла
–каждая 4-ка даёт 1 балл
–каждая 3-ка даёт 0 баллов
–каждая 2-ка даёт -1 балл Решение:
int[] marks = {5, 5, 4, 3, 3, 2, 5}; int sum = marks.Sum((x) =>
{
if (x == 5) return 2; if (x == 4) return 1; if (x == 2) return -1; return 0;
});
//sum = 6
Учебное задание 4.1.
Усовершенствовать учебное задание 3.1 так, чтобы в крестики-нолики можно было играть по сети.
Технология выполнения учебного задания 4.1.
Шаг 1. Создайте копию предыдущего проекта из задания 3.1, переименуйте проект и решение (только через окно « Обозревателя решений») в Lab41.
Шаг 2. Код кнопки « Новая игра» выделим в отдельный метод, метод назовите
NewGame. Сделайте это самостоятельно (вручную или с помощью инструментов рефакторинга).
Шаг 3. Удалите кнопку « Новая игра». Новая игра будет создаваться автоматически при подключении клиента или создании сервера.
Шаг 4. Добавим новые элементы управления, как показано на рисунке 1.
Рисунок 1. Размещение новых компонентов на форме
Ниже перечислены новые компоненты и их свойства:
∙Контейнер (GroupBox)
o свойство Name = “grNet”
oсвойство Text = “Настройки”
∙Этикетка(Label) (внутри контейнера)
oсвойство Text = “IP сервера:”
∙Тестовое поле (TextBox) (внутри контейнера)
o свойство Name = “txtIP”
oсвойство Text = “”
∙Кнопка « Подключиться» ( внутри контейнера)
oсвойство Name = “ btnConnect”
oсвойство Text = “ Подключиться”
∙Кнопка « Создать сервер» ( внутри контейнера)
oсвойство Name = “ btnServer”
o свойство Text = “ Создать сервер”
Шаг 5. Импортируем необходимые пространства имён. В этом уроке мы будем использовать классы TcpListner и TcpClient из пространства имён System.Net.Sockets
и потоки (Thread) из пространства имён System.Threading.
Добавьте код из листинга 1.
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets; using System.Threading;
namespace Lab41
{
public partial class fMain : Form
{
Листинг 1. Импортирование пространств имён.
Шаг 6. Создадим необходимые глобальные объекты на уровне формы fMain.
public partial class fMain : Form
{
public enum PlayerType
{
None,
X,
O
}
//Сервер. Может принимать входящие подключения.
//Просле подключения нового клиента возвращает объект класса TcpClient private TcpListener Server;
//"Удалённый" клиент(подключившийся). Этот объект позволяет //принимать\отправлять информацию со стороны сервера. private TcpClient remoteClient;
//"Локальный" клиент. Клиент, который подключаетя к серверу. //Этот объект позволяет принимать\отправлять информацию //со стороны клиента.
private TcpClient localClient;
//игрок за которого вы играете //(сервер всегда крестик, клиент всегда нолик)
private PlayerType You = PlayerType.None;
//поток клиента
private Thread thClient; //поток сервера
private Thread thServer;
Листинг 2. Глобальные объекты на уровне формы.
Шаг 7. Добавим код, позволяющий создавать сервер. Самая серверная часть приложения будет состоять из экземпляра класса TcpListener (объект Server) и TcpClient (remoteClient). Сам по себе объект Server может только принимать входящие подключения (в текущей реализации принимает только 1 подключение),
но не может сам отправлять\принимать данные.
За приём нового входящего подключения отвечает метод AcceptTcpClient. Это блокирующая операция, вызов метода приостанавливает текущий поток до подключения нового клиента, поэтому этот метод обычно вызывается в отдельном потоке. Метод AcceptTcpClient возвращает в качестве результата экземпляр класса
TcpClient, через который можно взаимодействовать (принимать\отправлять данные)
с подключившимся клиентом. При создании нового потока мы используем анонимную функцию и лямбда выражение ( () => {} ), такой подход позволяет создать новый метод внутри уже существующего метода (визуально), таким образом код выглядит более наглядно (вся серверная часть собрана в одном месте).
По событию Click кнопки btnServer добавьте код из листинга 3.
private void btnServer_Click(object sender, EventArgs e)
{
//ожидаем подключений со всех адресов на порт 6785 Server = new TcpListener(IPAddress.Any, 6785); //запускаем сервер
Server.Start();
//создаём новый поток (используем анонимную функцию и лямбда-выражение) thServer = new Thread(() =>
{
//далее идёт код потока который будет выполнятсья параллельно...
//ожидаем входящего подключения клиента (блокирующая операция) remoteClient = Server.AcceptTcpClient();
byte[] buffer = new byte[1024];
//клиент подключился - указываем, что мы играем крестикам You = PlayerType.X;
//цикл выполняется до тех пор пока есть активное соединение while (remoteClient.Connected)
{
//очищаем массив buffer (заполняем нулями) Array.Clear(buffer,0,buffer.Length);
//читаем информацию от клиента (блокирующая операция) remoteClient.GetStream().Read(buffer, 0, 1024); //получили новую информацию - переводим в строку string strData = Encoding.Default.GetString(buffer); //если сейчас ходят нолики (т.е. не мы)
if (ActivePlayer == PlayerType.O)
{
//разбиваем строку на массив элементов с разделителем пробелом string[] ij = strData.Split(' ');
//получаем координаты кнопки на которую кликнул противник int x = Convert.ToInt32(ij[0]);
int y = Convert.ToInt32(ij[1]);
//меняем у кнопки надпись на "O",
//т.к. этот компонент создан в другом потоке
//то его можно менять только через вызов метода Invoke. //Чтобы не создавать доп. метод используем лямбда-выражения. this.Invoke((Action)(() => { btnField[x, y].Text = "O"; })); //Удалённый игрок сделал ход... меняем активного игрока
if (ActivePlayer == PlayerType.X) ActivePlayer = PlayerType.O;
else
ActivePlayer = PlayerType.X;
}
}
});
//Запускаем сервер thServer.Start();
//отключаем панель создания сервера\подключения grNet.Enabled = false;
//первым ходят крестики ActivePlayer = PlayerType.X; //создаём игровое поле...
NewGame();
}
Листинг 3. Cобытие Click кнопки btnServer.
Шаг 8. Добавим код, позволяющий подключаться клиенту. По событию Click кнопки
btnConnect добавьте код из листинга 4.
private void btnConnect_Click(object sender, EventArgs e)
{
localClient = new TcpClient();
//подключаемся к заданному IP адресу на порт 6785 localClient.Connect(txtIP.Text, 6785);
//создаём новый поток (используем анонимную функцию и лямбда-выражение) thClient = new Thread(() =>
{
//цикл выполняется до тех пор пока есть активное соединение while (localClient.Connected)
{
byte[] buffer = new byte[1024];
//сервер подключился - указываем, что мы играем ноликами You = PlayerType.O;
while (localClient.Connected)
{
//очищаем массив buffer (заполняем нулями) Array.Clear(buffer, 0, buffer.Length);
//читаем информацию от сервера (блокирующая операция) localClient.GetStream().Read(buffer, 0, 1024); //получили новую информацию - переводим в строку string strData = Encoding.Default.GetString(buffer); //если сейчас ходят крестики (т.е. не мы)
if (ActivePlayer == PlayerType.X)
{
//разбиваем строку на массив элементов //с разделителем пробелом
string[] ij = strData.Split(' ');
//получаем координаты кнопки на которую кликнул противник int x = Convert.ToInt32(ij[0]);
int y = Convert.ToInt32(ij[1]); //меняем у кнопки надпись на "X",
//т.к. этот компонент создан в другом потоке
//его можно менять только через вызов метода Invoke //чтобы не создавать доп. метод используем лямбда-выражения
this.Invoke((Action)(() => { btnField[x, y].Text = "X"; }));
//Удалённый игрок сделал ход... меняем активного игрока if (ActivePlayer == PlayerType.X)
ActivePlayer = PlayerType.O; else
ActivePlayer = PlayerType.X;
}
}
}
}); //Запускаем поток клиента
thClient.Start();
//отключаем панель создания сервера\подключения grNet.Enabled = false;
//первым ходят крестики ActivePlayer = PlayerType.X; //создаём игровое поле...
NewGame();
}
Листинг 4. Cобытие Click кнопки btnConnect.
Шаг 9. Каждый раз когда мы кликаем на кнопку (делаем свой ход за крестики или
нолики) вызывается процедура-обработчик события btn_Click. В эту процедуру тоже
надо внести изменения, а именно – отправлять на клиент\сервер информацию о том на какую кнопку мы нажали.
На реализации этого этапа возникает небольшая проблема – по сети мы должны передавать координаты кнопки в массиве, однако в процедуре btn_Click мы не знаем этих координат, у нас есть только sender – переменная, которая ссылается на кнопку, на которую мы кликнули. Передавать sender по сети нет смысла, т.к.
технически ссылка на кнопку представляет собой адрес в памяти процесса, а в разных процессах (тем более на разных компьютерах) эти адреса будут различаться. Т.е. к примеру на клиенте кнопка с индексом [3,3] будет находиться по адресу 0x0012B534, а на сервере та же самая кнопка будет находиться по адресу
0x004360F1, а по адресу 0x0012B534 на сервере могут находиться другие данные,
либо эта память может быть вообще не выделена.
Одним из вариантов решения является создание 2- циклов по строкам и столбцам массива btnField и поиска кнопки (btnField[I,j] == sender), когда кнопка будет найдена,
мы будем знать её координаты i, j.
В целях упрощения алгоритма будем использовать другой подход к решению этой проблемы, он заключается в том, чтобы сохранять координаты кнопки внутри одного из свойств кнопки. Любой визуальный компонент имеет свойство Tag, которое как раз предназначено для сохранения пользовательских данных. Оно имеет базовый тип object, таким образом, может хранить данные любого типа.
Задавать свойство Tag для кнопки будет в методе NewGame (листинг 5).
//Создаём новую кнопку
Button btn = new Button(); //задаём её координаты btn.Left = 10 + i*32; btn.Top = 20 + j*32; btn.Width = 32; btn.Height = 32;
//Сохраняем в свойстве Tag индексы кнопки в массиве btnField btn.Tag = i.ToString() + " " + j.ToString();
//меняем шрифт кнопки на полужирный Arial размером 12 btn.Font = new Font("Arial", 12, FontStyle.Bold); //добавляем обработчик события Click
btn.Click += new EventHandler(btn_Click);
Листинг 5. Метод NewGame. Сохранение индексов массива в свойстве Tag.
Затем, добавим код из листинга 6 в процедуру обработчик события btn_Click.
Void btn_Click(object sender, EventArgs e)
{
//btn - кнопка на которую кликнули
Button btn = (Button)sender;
//если на кнопке пусто и текущий игрок это мы...
if (btn.Text == "" && You == ActivePlayer)
{
btn.Text = ActivePlayer.ToString();
//получаем координаты кнопки в массиве btnField //то что сохраняли в методе NewGame
string infoToSend = (string)btn.Tag;
//преобразовываем строку в байтовый массив для передачи byte[] dataToSend = Encoding.Default.GetBytes(infoToSend); //если мы являемся клиентом...
if (localClient != null && localClient.Connected)
{
//...то передаём данные (сервер их получает в потоке thServer) localClient.GetStream().Write(dataToSend, 0, dataToSend.Length);
}//если мы являемся сервером...
else if (remoteClient != null && remoteClient.Connected)
{
//...то передаём данные (клиент их получает в потоке thClient) remoteClient.GetStream().Write(dataToSend, 0, dataToSend.Length);
}
//Проверка на победителя...
PlayerType winner = CheckWinner(); if (winner != PlayerType.None)
Листинг 6. Cобытие btn_Click
Шаг 10. После завершения шага 9 программа уже является работоспособной(можно играть по сети), однако после закрытия главной формы приложения программа не завершится (её можно будет увидеть запущенной в диспетчере задач). Это происходит потому что при закрытии формы завершается основной поток приложения, а дополнительные потоки (thClient или thServer) остаются работать.
Поэтому при закрытии главной формы (по событию FormClosed) важно закрывать дополнительно созданные потоки. Чтобы остановить поток достаточно остановить сервер и закрыть активные подключения клиента, в этом случае в потоке произойдёт исключение и он завершится.
Создайте процедуру обработчик события FormClosed для формы fMain и добавьте в неё код из листинга 7.
private void fMain_FormClosed(object sender, FormClosedEventArgs e)
{
//если сервер создан (!= null), то - останавливаем сервер if (Server != null) Server.Stop();
//если к серверу подключился клиент - закрываем подключение
if (remoteClient != null && remoteClient.Connected) remoteClient.Close(); //если клиент подключился к серверу - закрываем подключение
if (localClient != null && localClient.Connected) localClient.Close();
}
Листинг 7. Cобытие FormClosed формы fMain
