Cетевое взаимодействие посредством TCP

83376d7e2450ea0124d9e9b441b16ee7.jpgПриветствую!

Продолжу серию постов посвященных программированию, на этот раз я хочу поговорить на тему сетевого взаимодействие посредством TCP соединения между .Net приложениями. Статья может быть полезна новичкам или тем кто еще не сталкивался с сетью по отношению к .Net. Полностью работоспособный пример прилагается: http://yadi.sk/d/EIYRsi2LMuj7C.

Подробности под катом.

Зачем нужна эта статья Конечно, на данный момент доступно большое количество разнообразных библиотек для сетевого взаимодействия, тот же WCF, но тем не менее, умение соединить два приложения, написанных в том числе и на разных языках программирования может быть полезно.Немного теории Сетевое соединение фактически представляет собой поток (stream), куда клиент записывает байты, а сервер считывает и наоборот.Соответственно, необходимо реализовать механизм команд, которые должны сериализоваться на передающей стороне и десериализоваться на принимающей.Моя реализация В общем виде команда представляет собой объект с двумя методами «ToBytes» и «FromBytes», а также набором свойств которые мы хотим передать принимающей стороне.SingleCommand public class SingleCommand: BaseCommand { public int IntField { get; set; } public decimal DecimalField { get; set; }

//преобразует объект в массив байт public override byte[] ToBytes () { //вычисляем длину команды const int messageLenght = sizeof (int) + sizeof (decimal);

//инициализируем массив байт в который будут сохраняться данные var messageData = new byte[messageLenght]; using (var stream = new MemoryStream (messageData)) { //записываем по очереди наши свойства var writer = new BinaryWriter (stream); writer.Write (IntField); writer.Write (DecimalField); return messageData; } }

//возвращает объект из массива байт, критически важно считывать данные в том же порядке что и были записаны public static SingleCommand FromBytes (byte[] bytes) { using (var ms = new MemoryStream (bytes)) { var br = new BinaryReader (ms); var command = new SingleCommand ();

command.IntField = br.ReadInt32(); command.DecimalField = br.ReadDecimal ();

return command; } } } При необходимости отправки команды содержащий свойство переменной длины, например строка, в свойствах необходимо указывать длину этой строки: StringCommand public class StringCommand: BaseCommand { //длина передаваемой строки private int StringFieldLenght { get; set; } //строка public string StringField { get; set; }

public override byte[] ToBytes () { //преобразуем строку к массиву байт byte[] stringFieldBytes = CommandUtils.GetBytes (StringField);

//задаем ко-во байт строки StringFieldLenght = stringFieldBytes.Length;

//вычисляем длину команды в байтах int messageLenght = sizeof (int) + StringFieldLenght;

var messageData = new byte[messageLenght]; using (var stream = new MemoryStream (messageData)) { var writer = new BinaryWriter (stream); //первым делом записываем длину строки writer.Write (StringFieldLenght);

//записываем саму строки writer.Write (stringFieldBytes); return messageData; } }

public static StringCommand FromBytes (byte[] bytes) { using (var ms = new MemoryStream (bytes)) { var br = new BinaryReader (ms); var command = new StringCommand ();

//считываем из потока длину строки command.StringFieldLenght = br.ReadInt32(); //считываем из потока указанное количества байт и преобразуем в строку command.StringField = CommandUtils.GetString (br.ReadBytes (command.StringFieldLenght));

return command; } } } Чтобы принимающая сторона узнала что за команда пришла, необходимо перед отправкой команды отослать заголовок, который указывает количество (в примере этот момент упущен, прием идет только по одной команде) и тип команды: CommandHeader public struct CommandHeader { // тип команды, соответствует перечислению CommandTypeEnum public int Type { get; set; }

// количество команд public int Count { get; set; }

public static int GetLenght () { return sizeof (int) * 2; }

public static CommandHeader FromBytes (byte[] bytes) { using (var ms = new MemoryStream (bytes)) { var br = new BinaryReader (ms); var currentObject = new CommandHeader ();

currentObject.Type = br.ReadInt32(); currentObject.Count = br.ReadInt32();

return currentObject; } }

public byte[] ToBytes () { var data = new byte[GetLenght ()];

using (var stream = new MemoryStream (data)) { var writer = new BinaryWriter (stream); writer.Write (Type); writer.Write (Count); return data; } } } В моем случае, взаимодействие сервера с клиентом, происходит по следующему алгоритму:1. Клиент создает подключение.2. Отправляет команду3. Получает ответ.4. Закрывает соединение.5. Если ответ от сервера не пришел, отключается по таймауту.Отправка команды серверу:

Вызов метода отправки команды на сервер //создаем команду содержащую строку текста var stringCommand = new StringCommand { StringField = stringCommandTextBox.Text };

//отправляем на локальный сервер CommandSender.SendCommandToServer (»127.0.0.1», stringCommand, CommandTypeEnum.StringCommand); Тело метода команды отправки на сервер public static void SendCommandToServer (string serverIp, BaseCommand command, CommandTypeEnum typeEnum) { //создаем заголовок команды, которые указывает тип и количество var commandHeader = new CommandHeader { Count = 1, Type = (int)typeEnum };

//соединяем заголовок и саму команду byte[] commandBytes = CommandUtils.ConcatByteArrays (commandHeader.ToBytes (), command.ToBytes ());

//отправляем на сервер SendCommandToServer (serverIp, Settings.Port, commandBytes); }

private static void SendCommandToServer (string ipAddress, int port, byte[] messageBytes) { var client = new TcpClient (); try { client.Connect (ipAddress, port);

//добавляем 4 байта указывающие на длину команды byte[] messageBytesWithEof = CommandUtils.AddCommandLength (messageBytes); NetworkStream networkStream = client.GetStream (); networkStream.Write (messageBytesWithEof, 0, messageBytesWithEof.Length);

//получаем и парсим от сервера ответ MessageHandler.HandleClientMessage (client); } catch (SocketException exception) { Trace.WriteLine (exception.Message + » » + exception.InnerException); } } Получение команд от клиентов на стороне сервера public class CommandListener { private readonly TcpListener _tcpListener; private Thread _listenThread; private bool _continueListen = true;

public CommandListener () { //слушаем любой интерфейс на указанном порту _tcpListener = new TcpListener (IPAddress.Any, Settings.Port); }

public void Start () { //прием команд ведется в отдельном потоке _listenThread = new Thread (ListenForClients); _listenThread.Start (); }

private void ListenForClients () { _tcpListener.Start ();

while (_continueListen) { TcpClient client = _tcpListener.AcceptTcpClient ();

//обработка каждой отдельной команды ведется в отдельном потоке var clientThread = new Thread (HandleClientCommand); clientThread.Start (client); } _tcpListener.Stop (); }

private void HandleClientCommand (object client) { //обработка команд MessageHandler.HandleClientMessage (client); }

public void Stop () { _continueListen = false; _tcpListener.Stop (); _listenThread.Abort (); } } Обработка полученных команд: public static void HandleClientMessage (object client) { var tcpClient = (TcpClient)client;

//задаем таймаут в три секунды tcpClient.ReceiveTimeout = 3;

//получаем поток NetworkStream clientStream = tcpClient.GetStream ();

var ms = new MemoryStream (); var binaryWriter = new BinaryWriter (ms);

var message = new byte[tcpClient.ReceiveBufferSize]; var messageLenght = new byte[4]; int readCount; int totalReadMessageBytes = 0;

//получаем общую длину сообщения clientStream.Read (messageLenght, 0, 4);

//преобразуем к целому числу int messageLength = CommandUtils.BytesToInt (messageLenght);

//считываем данные из потока пока не дошли до конца сообщения while ((readCount = clientStream.Read (message, 0, tcpClient.ReceiveBufferSize)) != 0) { binaryWriter.Write (message, 0, readCount); totalReadMessageBytes += readCount; if (totalReadMessageBytes >= messageLength) break; }

if (ms.Length > 0) { //парсим полученные байты Parse (ms.ToArray (), tcpClient); } }

private static void Parse (byte[] bytes, TcpClient tcpClient) { if (bytes.Length >= CommandHeader.GetLenght ()) { //десериализуем заголовок команду CommandHeader commandHeader = CommandHeader.FromBytes (bytes); IEnumerable nextCommandBytes = bytes.Skip (CommandHeader.GetLenght ());

// в зависимости от типа десериализуем тут или иную команду switch ((CommandTypeEnum)commandHeader.Type) { case CommandTypeEnum.StringCommand: StringCommand stringCommand = StringCommand.FromBytes (nextCommandBytes.ToArray ()); if (OnStringCommand!= null) OnStringCommand (stringCommand, tcpClient); break; case CommandTypeEnum.MessageAccepted: if (OnMessageAccepted!= null) OnMessageAccepted (); break; } } } Взаимодействие с Java Команда передает на сервер одно значениеКоманда package com.offviewclient.network.commands;

import java.io.*;

public class IntCommand implements Serializable {

public int IntNumber;

public static int GetLenght () { return 4; }

public static IntCommand FromBytes (byte[] bytes) throws IOException { ByteArrayInputStream inputStream = new ByteArrayInputStream (bytes); DataInputStream ois = new DataInputStream (inputStream);

IntCommand commandType = new IntCommand (); commandType.IntNumber = ois.readInt (); return commandType; }

public byte[] ToBytes () throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream (); DataOutputStream oos = new DataOutputStream (bos); oos.writeInt (this.IntNumber); byte[] yourBytes = bos.toByteArray (); oos.close (); bos.close (); return yourBytes; } } Отправка команды и получение ответа от сервера (код из рабочего проекта) : private void SendPacket (byte[] packetBytes) throws IOException {

byte[] packetBytesWithEOF = CommandUtils.AddCommandLength (packetBytes);

Socket socket = new Socket (serverIP, port); socket.setSoTimeout (5000);

OutputStream socketOutputStream = socket.getOutputStream (); socketOutputStream.write (packetBytesWithEOF);

byte[] answerBytes = ReadAnswerBytes (socket); socket.close (); Parse (answerBytes); }

private byte[] ReadAnswerBytes (Socket socket) throws IOException { InputStream out = socket.getInputStream (); DataInputStream dis = new DataInputStream (out);

ByteArrayOutputStream bos = new ByteArrayOutputStream (); DataOutputStream binaryWriter = new DataOutputStream (bos);

int readCount; byte[] message = new byte[10000]; byte[] messageLength = new byte[4];

dis.read (messageLength, 0, 4); int messageLength = CommandUtils.BytesToInt (messageLength);

int totalReadMessageBytes = 0;

while ((readCount = dis.read (message, 0, 10000)) != 0) { binaryWriter.write (message, 0, readCount); totalReadMessageBytes += readCount; if (totalReadMessageBytes >= messageLength) break; } return bos.toByteArray (); }

private void Parse (byte[] messageBytes) throws IOException { if (messageBytes.length >= CommandHeader.GetLenght ()) { CommandHeader commandType = CommandHeader.FromBytes (messageBytes);

int skipBytes = commandType.GetLenght ();

if (commandType.Type == CommandTypeEnum.MESSAGE_ACCEPTED) { RiseMessageAccepted (); }

if (commandType.Type == CommandTypeEnum.SLIDE_PAGE_BYTES) { List drawableList = new Vector();

for (int i = 0; i< commandType.Count; i++) { PresentationSlideCommand presentationSlideCommand = PresentationSlideCommand.FromBytes(messageBytes, skipBytes); drawableList.add(presentationSlideCommand.FileBytes); skipBytes += presentationSlideCommand.GetLenght(); }

RiseMessageAcceptSlideEvent (drawableList); } } } Важный момент при взаимодействии Java и .Net: java хранить байты элементарных типов по отношению к .Net наоборот, поэтому на стороне все числовые значение надо разворачивать вызовом метода IPAddress.HostToNetworkOrder: Надеюсь, что это все будет кому-то полезным. Демо проект на яндекс диске: http://yadi.sk/d/EIYRsi2LMuj7C.Всем спасибо!

© Habrahabr.ru