Приветствую!
Продолжу серию постов посвященных программированию, на этот раз я хочу поговорить на тему сетевого взаимодействие посредством TCP соединения между .Net приложениями. Статья может быть полезна новичкам или тем кто еще не сталкивался с сетью по отношению к .Net. Полностью работоспособный пример прилагается: http://ift.tt/1r9s4O5.
Зачем нужна эта статья
Конечно, на данный момент доступно большое количество разнообразных библиотек для сетевого взаимодействия, тот же 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<byte> 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<byte[]> drawableList = new Vector<byte[]>();
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://ift.tt/1r9s4O5.
Всем спасибо!
This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий