📜 ⬆️ ⬇️

Networking via TCP in C # - your bike



Greetings

I will continue a series of posts devoted to programming, this time I want to talk about networking via a TCP connection between .Net applications. The article may be useful for beginners or those who have not yet encountered the network in relation to .Net. A fully functional example is attached: http://yadi.sk/d/1OxmAFuCN3kmc .
')
Details under the cut.

Why this article is needed

Of course, at the moment there are a large number of different libraries available for networking, the same WCF, but nevertheless, the ability to connect two applications written in different programming languages ​​can be useful.

Some theory

The network connection is actually a stream (stream) where the client writes bytes, and the server reads and vice versa.
Accordingly, it is necessary to implement the mechanism of commands that must be serialized on the transmitting side and deserialized on the receiving side.

My implementation

In general, a command is an object with two methods “ToBytes” and “FromBytes”, as well as a set of properties that we want to pass to the receiving side.
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; } } } 


If you need to send a command containing a variable-length property, for example, a string, you must specify the length of this string in the properties:
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; } } } 


In order for the receiving party to find out what kind of command it is, you need to send a header that indicates the number (in the example this moment is missed, only one command is received) and the type of 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; } } } 


In my case, the server interaction with the client, occurs according to the following algorithm:
1. The client creates a connection.
2. Sends a command
3. Receives the answer.
4. Closes the connection.
5. If the answer from the server did not come, it is disconnected on timeout.

Sending a command to the server:
Calling the command sending method to the server
 //     var stringCommand = new StringCommand { StringField = stringCommandTextBox.Text }; //    CommandSender.SendCommandToServer("127.0.0.1", stringCommand, CommandTypeEnum.StringCommand); 


The body of the send command method
 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); } } 


Receive commands from server-side clients
 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(); } } 


Processing received commands:
 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 interaction

The command sends one value to the server.
Team
 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; } } 


Sending a command and receiving a response from the server (code from the working draft):
  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); } } } 


An important point in the interaction of Java and .Net: java store bytes of elementary types with respect to .Net, on the contrary, therefore, on the .Net side, all numeric values ​​must be expanded by calling the IPAddress.HostToNetworkOrder method:

Using

As an example, we will teach the server to accept the command to save the user. User details will contain first and last name.
For this you need:
1. Add a new command to the listing
 public enum CommandTypeEnum : int { StringCommand = 1, MessageAccepted = 2, SingleCommand = 3, FileCommand = 4, SaveUserCommand = 5 //     } 

2. Add data transmitted with the command:
 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. Complete the MessageHandler with a new case.
 case CommandTypeEnum.SaveUserCommand: if (OnSingleCommand != null) OnSaveUserCommand((SaveUserCommand)baseCommand, tcpClient); break; 

4. Add a new mapping to the deserialization delegate for 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. We send a new team on the client side:
 var saveUserCommand = new SaveUserCommand { FirstName = firstNameTextBox.Text, SecondName = secondNameTextBox.Text }; CommandSender.SendCommandToServer(serverIpTextBox.Text, saveUserCommand, CommandTypeEnum.SaveUserCommand); 

6. Accept on the server side:
 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); } 


I hope that this will all be useful to someone. Demo project on Yandex disk: http://yadi.sk/d/1OxmAFuCN3kmc .
Thanks to all!

Source: https://habr.com/ru/post/220285/


All Articles