Pull to refresh

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

Reading time 10 min
Views 50K


Приветствую!

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

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

Зачем нужна эта статья

Конечно, на данный момент доступно большое количество разнообразных библиотек для сетевого взаимодействия, тот же 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());

        var commandTypeEnum = (CommandTypeEnum)commandHeader.Type;

        if (commandTypeEnum == CommandTypeEnum.MessageAccepted)
        {
            if (OnMessageAccepted != null)
                OnMessageAccepted();
        }
        else
        {
            BaseCommand baseCommand = BytesToCommands[commandTypeEnum].Invoke(nextCommandBytes.ToArray());

            switch (commandTypeEnum)
            {
                case CommandTypeEnum.StringCommand:
                    if (OnStringCommand != null)
                        OnStringCommand((StringCommand)baseCommand, tcpClient);
                    break;
                case CommandTypeEnum.SingleCommand:
                    if (OnSingleCommand != null)
                        OnSingleCommand((SingleCommand)baseCommand, tcpClient);
                    break;
                case CommandTypeEnum.FileCommand:
                    if (OnSingleCommand != null)
                        OnFileCommand((FileCommand)baseCommand, tcpClient);
                    break;
                case CommandTypeEnum.SaveUserCommand:
                    if (OnSingleCommand != null)
                        OnSaveUserCommand((SaveUserCommand)baseCommand, tcpClient);
                    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 наоборот, поэтому на стороне .Net все числовые значение надо разворачивать вызовом метода IPAddress.HostToNetworkOrder:

Использование

В качестве примера, научим сервер принимать команду на сохранение пользователя. Данные пользователя будут содержать имя и фамилию.
Для этого нужно:
1. Добавить новую команду в перечисление
public enum CommandTypeEnum : int
{
    StringCommand = 1,
    MessageAccepted = 2,
    SingleCommand = 3,
    FileCommand = 4,
    SaveUserCommand = 5 // команда на сохранение пользователя
}

2. Добавить данные передающиеся с командой:
public class SaveUserCommand : BaseCommand
{
    private int FirstNameLenght { get; set; }
    public string FirstName { get; set; } //имя пользователя

    private int SecondNameLenght { get; set; }
    public string SecondName { get; set; } //фамилия

    public override byte[] ToBytes()
    {
        byte[] firstNamebytes = CommandUtils.GetBytes(FirstName);
        FirstNameLenght = firstNamebytes.Length;

        byte[] secondNamebytes = CommandUtils.GetBytes(SecondName);
        SecondNameLenght = secondNamebytes.Length;

        int messageLenght = sizeof(int) * 2 + FirstNameLenght + SecondNameLenght; // длина сообщения

        var messageData = new byte[messageLenght];
        using (var stream = new MemoryStream(messageData))
        {
            var writer = new BinaryWriter(stream);
            writer.Write(FirstNameLenght);
            writer.Write(firstNamebytes);

            writer.Write(SecondNameLenght);
            writer.Write(secondNamebytes);

            return messageData;
        }
    }

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

            command.FirstNameLenght = br.ReadInt32();
            command.FirstName = CommandUtils.GetString(br.ReadBytes(command.FirstNameLenght));

            command.SecondNameLenght = br.ReadInt32();
            command.SecondName = CommandUtils.GetString(br.ReadBytes(command.SecondNameLenght));

            return command;
        }
    }
}

3. Дополнить MessageHandler новым кейсом
case CommandTypeEnum.SaveUserCommand:
    if (OnSingleCommand != null)
        OnSaveUserCommand((SaveUserCommand)baseCommand, tcpClient);
    break;

4. Дополнить MessageHandler новым маппингом на делегат десериализации
private static void FillBytesToCommandsDictionary()
{
    BytesToCommands.Add(CommandTypeEnum.StringCommand, StringCommand.FromBytes);
    BytesToCommands.Add(CommandTypeEnum.SingleCommand, SingleCommand.FromBytes);
    BytesToCommands.Add(CommandTypeEnum.FileCommand, FileCommand.FromBytes);
    BytesToCommands.Add(CommandTypeEnum.SaveUserCommand, SaveUserCommand.FromBytes);
}

5. Отправляем новую команду на стороне клиента:
var saveUserCommand = new SaveUserCommand
{
    FirstName = firstNameTextBox.Text,
    SecondName = secondNameTextBox.Text
};

CommandSender.SendCommandToServer(serverIpTextBox.Text, saveUserCommand, CommandTypeEnum.SaveUserCommand);

6. Принимаем на стороне сервера:
MessageHandler.OnSaveUserCommand += CommandListener_OnSaveUserCommand;

private static void CommandListener_OnSaveUserCommand(SaveUserCommand saveUserCommand, TcpClient tcpClient)
{
    Console.WriteLine("SaveUserCommand accepted, FirstName:{0}, SecondName:{1}", saveUserCommand.FirstName, saveUserCommand.SecondName);
    CommandSender.SendMessageAcceptedToClient(tcpClient);
}


Надеюсь, что это все будет кому-то полезным. Демо проект на яндекс диске: http://yadi.sk/d/1OxmAFuCN3kmc.
Всем спасибо!
Tags:
Hubs:
+2
Comments 9
Comments Comments 9

Articles