Введение в сетевое программирование

Материал из Вики ИТ мехмата ЮФУ
Перейти к: навигация, поиск

Введение в сетевое программирование

Постановка задачи

Для знакомства с сетевым программированием разберем пример. Пусть требуется запрограммировать службу удаленных вычислений. Клиенты просят сервер вычислить выражение (для начала содержащее только одну арифметическую операцию +, -, *, / ), сервер возвращает результат.

Разработка протокола

Назовем наш протокол Calculation 0.1. Для начала определим формат запроса и ответа. Пусть запрос клиента должен начинаться со слова CALC, далее через пробел операция и потом два числа — аргументы. Тогда запрос клиента на вычисление произведения 12 и 6 будет выглядеть так:

CALC * 12 6 ENTER

Ответ сервера будет начинаться со слова OK, если запрос был корректен и далее через пробел число – результат вычислений. Если же запрос некорректен, ответом будет слово ERR.

ОК 72 ENTER (если все нормально)
ERR ENTER (если запрос неправильный)

ENTER нужен, чтобы узнать где конец строки.

После определения правил работы протокола, можно перейти к реализации. И тут встает вопрос: используем TCP или UDP? Мы разберем оба случая. И, для начала, разберем алгоритм взаимодействия сервера с клиентом, а потом реализуем его программно.

Алгоритм работы сервера (TCP)

  1. Запускается заранее, до подключения клиентов.
  2. Сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт №12345.
  3. Выделяет память для очереди подключений.
  4. В цикле:
  • устанавливает соединение с клиентом из очереди; если очередь пуста, то ждет подключения клиента;
  • принимает/передает данные;
  • закрывает соединение с клиентом.

Алгоритм работы клиента (TCP)

  1. Получает от ОС случайный номер порта для общения с сервером.
  2. Устанавливает соединение с сервером.
  3. Передает/принимает данные.
  4. Закрывает соединение с сервером.

Алгоритм работы сервера (UDP)

  1. Запускается заранее, до подключения клиентов.
  2. сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт №12345.
  3. В цикле:
  • ждет прихода сообщения;
  • обрабатывает данные;
  • передает результат.

Основное отличие UDP от TCP — не нужно возиться с соединениями.

Алгоритм работы клиента (UDP)

  1. Получает от ОС случайный номер порта для общения с сервером.
  2. Передает/принимает данные.

TCP-сервер (C++)

Будем использовать winsock2.h

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

// В опциях проекта, в разделе Linker, в пункте Additional Dependancies укажите Ws2_32.lib

int __cdecl main(void) 
{
	WSADATA wsaData;
	SOCKET ListenSocket,ClientSocket;  // впускающий сокет и сокет для клиентов
	sockaddr_in ServerAddr;  // это будет адрес сервера
	int err, maxlen = 512;  // код ошибки и размер буферов
	char* recvbuf=new char[maxlen];  // буфер приема
	char* result_string=new char[maxlen];  // буфер отправки


	// Initialize Winsock
	WSAStartup(MAKEWORD(2,2), &wsaData);

	// Create a SOCKET for connecting to server
	ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	// Setup the TCP listening socket
	ServerAddr.sin_family=AF_INET;
	ServerAddr.sin_addr.s_addr=inet_addr("127.0.0.1");
	ServerAddr.sin_port=htons(12345);
	err = bind( ListenSocket, (sockaddr *) &ServerAddr, sizeof(ServerAddr));
	if (err == SOCKET_ERROR) {
		printf("bind failed: %d\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}

	err = listen(ListenSocket, 50);
	if (err == SOCKET_ERROR) {
		printf("listen failed: %d\n", WSAGetLastError());
		closesocket(ListenSocket);
		WSACleanup();
		return 1;
	}
	while (true) {
		// Accept a client socket
		ClientSocket = accept(ListenSocket, NULL, NULL);
		err = recv(ClientSocket, recvbuf, maxlen, 0);
		if (err > 0) {
			recvbuf[err]=0;
			printf("Received query: %s\n", (char* )recvbuf);
			// вычисляем результат
			int result=72;
			_snprintf_s(result_string,maxlen,maxlen,"OK %d\n",result);
			// отправляем результат на сервер
			send( ClientSocket,  result_string, strlen(result_string), 0 );
			printf("Sent answer: %s\n", result_string);
		}
		else if (err == 0)
			printf("Connection closing...\n");
		else  {
			printf("recv failed: %d\n", WSAGetLastError());
			closesocket(ClientSocket);
			WSACleanup();
			return 1;
		}

		// shutdown the connection since we're done
		closesocket(ClientSocket);
	}
}

TCP-клиент (C++)

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

// В опциях проекта, в разделе Linker, в пункте Additional Dependancies укажите Ws2_32.lib

int __cdecl main(void) 
{
	WSADATA wsaData;
	SOCKET ConnectSocket;  // впускающий сокет и сокет для клиентов
	sockaddr_in ServerAddr;  // это будет адрес сервера
	int err, maxlen = 512;  // код ошибки и размер буферов
	char* recvbuf=new char[maxlen];  // буфер приема
	char* query=new char[maxlen];  // буфер отправки


	// Initialize Winsock
	WSAStartup(MAKEWORD(2,2), &wsaData);

	// Connect to server
	ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	ServerAddr.sin_family=AF_INET;
	ServerAddr.sin_addr.s_addr=inet_addr("127.0.0.1");
	ServerAddr.sin_port=htons(12345);

	err = connect( ConnectSocket, (sockaddr *) &ServerAddr, sizeof(ServerAddr));

	if (err == SOCKET_ERROR) {
		printf("connect failed: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}

	_snprintf_s(query,maxlen,maxlen,"CALC * 12 6\n");
	// отправляем запрос на сервер
	send( ConnectSocket,  query, strlen(query), 0 );
	printf("Sent: %s\n", query);

	// получаем результат
	err = recv(ConnectSocket, recvbuf, maxlen, 0);
	if (err > 0) {
		recvbuf[err]=0;
		printf("Result: %s\n", (char* )recvbuf);
	}
	else if (err == 0)
		printf("Connection closing...\n");
	else  {
		printf("recv failed: %d\n", WSAGetLastError());
		closesocket(ConnectSocket);
		WSACleanup();
		return 1;
	}

	// shutdown the connection since we're done
	closesocket(ConnectSocket);
	
}

UDP-сервер (C++)

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

// В опциях проекта, в разделе Linker, в пункте Additional Dependancies укажите Ws2_32.lib

int __cdecl main(void) 
{
	WSADATA wsaData;
	SOCKET SendRecvSocket;  // сокет для приема и передачи
	sockaddr_in ServerAddr, ClientAddr;  // это будет адрес сервера и клиентов
	int err, maxlen = 512, ClientAddrSize=sizeof(ClientAddr);  // код ошибки, размер буферов и размер структуры адреса
	char* recvbuf=new char[maxlen];  // буфер приема
	char* result_string=new char[maxlen];  // буфер отправки


	// Initialize Winsock
	WSAStartup(MAKEWORD(2,2), &wsaData);

	// Create a SOCKET for connecting to server
	SendRecvSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

	// Setup the TCP listening socket
	ServerAddr.sin_family=AF_INET;
	ServerAddr.sin_addr.s_addr=inet_addr("127.0.0.1");
	ServerAddr.sin_port=htons(12345);
	err = bind( SendRecvSocket, (sockaddr *) &ServerAddr, sizeof(ServerAddr));
	if (err == SOCKET_ERROR) {
		printf("bind failed: %d\n", WSAGetLastError());
		closesocket(SendRecvSocket);
		WSACleanup();
		return 1;
	}

	while (true) {
		// Accept a client socket
		err = recvfrom(SendRecvSocket,recvbuf,maxlen,0,(sockaddr *)&ClientAddr,&ClientAddrSize);
		if (err > 0) {
			recvbuf[err]=0;
			printf("Received query: %s\n", (char* )recvbuf);
			// вычисляем результат
			int result=72;
			_snprintf_s(result_string,maxlen,maxlen,"OK %d\n",result);
			// отправляем результат на сервер
			sendto(SendRecvSocket,result_string,strlen(result_string),0,(sockaddr *)&ClientAddr,sizeof(ClientAddr));
			printf("Sent answer: %s\n", result_string);
		}
		else  {
			printf("recv failed: %d\n", WSAGetLastError());
			closesocket(SendRecvSocket);
			WSACleanup();
			return 1;
		}
	}
}

UDP-клиент (C++)

#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

// В опциях проекта, в разделе Linker, в пункте Additional Dependancies укажите Ws2_32.lib

int __cdecl main(void) 
{
	WSADATA wsaData;
	SOCKET SendRecvSocket;  // сокет для приема и передачи
	sockaddr_in ServerAddr;  // это будет адрес сервера и клиентов
	int err, maxlen = 512;  // код ошибки, размер буферов и размер структуры адреса
	char* recvbuf=new char[maxlen];  // буфер приема
	char* query=new char[maxlen];  // буфер отправки


	// Initialize Winsock
	WSAStartup(MAKEWORD(2,2), &wsaData);

	// Create a SOCKET for connecting to server
	SendRecvSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

	ServerAddr.sin_family=AF_INET;
	ServerAddr.sin_addr.s_addr=inet_addr("127.0.0.1");
	ServerAddr.sin_port=htons(12345);

	_snprintf_s(query,maxlen,maxlen,"CALC * 12 6\n");
	// отправляем запрос на сервер
	sendto(SendRecvSocket,query, strlen(query), 0, (sockaddr *)&ServerAddr,sizeof(ServerAddr));  
	printf("Sent: %s\n", query);

	// получаем результат
	err = recvfrom(SendRecvSocket,recvbuf,maxlen,0,0,0);
	if (err > 0) {
		recvbuf[err]=0;
		printf("Result: %s\n", (char* )recvbuf);
	}
	else {
		printf("recv failed: %d\n", WSAGetLastError());
		closesocket(SendRecvSocket);
		WSACleanup();
		return 1;
	}

	closesocket(SendRecvSocket);

}

TCP-сервер (C#, .NET)

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

class TCP_Server
{
    public static void Main()
    {
        TcpListener server = null;
        try
        {
            Int32 port = 12345; //порт сервера
            IPAddress localAddr = IPAddress.Parse("127.0.0.1");//ip-адрес сервера (интерфейс)
            
            //TcpListener - класс TCP-сервера из .Net Framework Class Library
            server = new TcpListener(localAddr, port);

            // начинаем ожидание подсоединений клиентов на интерфейсе localAddr и порту port
            server.Start();

            // буффер для приема сообщений и соответствующая ему строка для вывода на экран
            Byte[] bytes = new Byte[1000];
            String data;
            
            //ответ клиенту
            String answer_message;

            //цикл обработки подсоединений клиентов
            while (true)
            {
                Console.Write("Waiting for a connection... ");
                // Ждем соединения клиента
                TcpClient client = server.AcceptTcpClient();
                //Ура! Кто-то подсоединился!
                Console.WriteLine("Connected!");
                // вводим поток stream для чтения и записи через установленное соединение
                NetworkStream stream = client.GetStream();
                int i = stream.Read(bytes, 0, bytes.Length);
                if (i > 0)
                {
                    // преобразуем принятые данные в строку ASCII string.
                    data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
                    //печатаем то, что получили
                    Console.WriteLine("Received: {0}", data);
                    //анализируем запрос клиента и вычисляем результат
                    int res = 72;
                    answer_message = "OK " + res.ToString() + (char)13 + (char)10;
                    //печатаем то, что будем отправлять
                    Console.WriteLine("Sent: {0}", answer_message);
                    //преобразуем строчку-ответ сервера в массив байт
                    byte[] msg = System.Text.Encoding.ASCII.GetBytes(answer_message);
                    // отправляем ответ
                    stream.Write(msg, 0, msg.Length);
                }

                // закрываем соединение
                client.Close();
            }
        }
        catch (SocketException expt)
        {
            Console.WriteLine("SocketException: {0}", expt);
        }
        finally
        {
            // Stop listening for new clients.
            server.Stop();
        }

        Console.WriteLine("\nHit enter to continue...");
        Console.Read();
    }
}

TCP-клиент (C#, .NET)

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;

namespace ClientCSharp
{
    class TCP_Client
    {
        static void Main(string[] args)
        {
            try
            {
                Int32 port = 12345;//порт сервера
                string message = "CALC * 12 6\n";//строка, которую пошлем серверу
                TcpClient client = new TcpClient("localhost", port);

                //преобразуем строчку в массив байт
                Byte[] data = System.Text.Encoding.ASCII.GetBytes(message);

                // вводим поток stream для чтения и записи через установленное соединение                
                NetworkStream stream = client.GetStream();

                // посылаем сообщение серверу 
                stream.Write(data, 0, data.Length);

                Console.WriteLine("Sent: {0}", message);//печатаем то, что отправили

                // буффер для приема сообщений
                data = new Byte[1000];

                // строка для приема сообщений сервера
                String responseData;

                // получаем сообщение от сервера
                Int32 bytes = stream.Read(data, 0, data.Length);
                responseData = System.Text.Encoding.ASCII.GetString(data, 0, bytes);
                //печатаем то, что получили
                Console.WriteLine("Received: {0}", responseData);

                // закрываем соединение
                stream.Close();
                client.Close();
            }
            catch (ArgumentNullException expt)
            {
                Console.WriteLine("ArgumentNullException: {0}", expt);
            }
            catch (SocketException expt)
            {
                Console.WriteLine("SocketException: {0}", expt);
            }

            Console.WriteLine("\n Press Enter to continue...");
            Console.Read();
        }
    }
}