Введение в сетевое программирование — различия между версиями

Материал из Вики ИТ мехмата ЮФУ
Перейти к: навигация, поиск
(Введение в сетевое программирование: Убрала сокеты на транспортный уровень, где им и место (но пока там не сохраняла изсенений))
 
(не показана 1 промежуточная версия этого же участника)
Строка 1: Строка 1:
 
[[Категория:Компьютерные сети]]
 
[[Категория:Компьютерные сети]]
 
==Введение в сетевое программирование==
 
==Введение в сетевое программирование==
===Интерфейс транспортного уровня===
 
=== Сокеты===
 
'''Socket''' (гнездо) – это структура данных, идентифицирующая сетевое соединение.
 
 
Зачем нужны эти сокеты? Сервер (программа) может одновременно поддерживать несколько TCP-соединений с другими компьютерами, используя один и тот же стандартный номер порта. Как это реализовать? Можно возложить эту задачу на программиста. Пусть он выбирает из буфера приема сетевого уровня пакеты, смотрит от кого они отправлены и отвечает соответствующим образом. Но можно сделать все это удобнее.
 
 
С каждым соединением должен быть связан свой поток, в который можно писать информацию и из которого можно ее считывать. Каждому потоку соответствует свой IP-адрес удаленного компьютера и свой порт удаленного компьютера. Будем назвать структуру данных, соответствующую каждому такому потоку, сокетом (розеткой). Таким образом, сервер можно сравнить с разветвителем с кучей розеток, к которым подключены клиенты.
 
Если сделать так, то вместо того, чтобы разбираться в куче разносортных пакетов из буфера приема сетевого уровня, сервер будет читать из потоков, каждый из которых соответствует своему клиенту. Данные от клиентов не будут сваливаться в кучу, а будут распределяться по потокам-сокетам. Ответственность за такое распределение ложится не на программиста, а на драйвер транспортного уровня операционной системы.
 
Сокеты были разработаны в университете в Berkley. Стали стандартом, вместо OSI-шного TLI (Transport Layer Interface).
 
 
{{Hider
 
|title=Историческая справка. Раскол UNIX
 
|content=
 
С 1978 года начинает свою историю BSD UNIX , созданный в университете Беркли. Автором BSD был Билл Джой. В начале 1980-х компания AT&T, которой принадлежали Bell Labs, осознала ценность UNIX и начала создание коммерческой версии UNIX . Важной причиной раскола UNIX стала реализация в 1980 г. стека протоколов TCP/IP. До этого межмашинное взаимодействие в UNIX пребывало в зачаточном состоянии — наиболее существенным способом связи был UUCP (средство копирования файлов из одной UNIX-системы в другую, изначально работавшее по телефонным сетям с помощью модемов).
 
Эти две операционные системы реализовали 2 различных интерфейса программирования сетевых приложений: Berkley sockets (TCP/IP) и интерфейс транспортного  уровня TLI (OSI ISO) (англ. Transport Layer Interface). Интерфейс Berkley sockets был разработан в университете Беркли и использовал стек протоколов TCP/IP, разработанный там же. TLI был создан AT&T в соответствии с определением транспортного  уровня модели OSI. Первоначально в ней не было реализации TCP/IP или других сетевых протоколов, но подобные реализации предоставлялись сторонними фирмами. Это, как и другие соображения (по большей части, рыночные), вызвало окончательное размежевание между двумя ветвями UNIX — BSD (университета Беркли) и System V (коммерческая версия от AT&T). Впоследствии, многие компании, лицензировав System V у AT&T, разработали собственные коммерческие разновидности UNIX, такие, как AIX, HP-UX, IRIX, Solaris.
 
}}
 
 
===Команды===
 
SOCKET – создать новый (пустой) сокет.
 
 
BIND – сервер связывает свой локальный адрес (порт) с сокетом.
 
 
LISTEN – сервер выделяет память под очередь подсоединений клиентов (TCP).
 
 
ACCEPT – сервер ожидает подсоединения клиента или принимает первое подключение из очереди (TCP). Чтобы заблокировать ожидание входящих соединений, сервер выполняет примитив ACCEPT. Получив запрос соединения, транспортный модуль ОС создает новый сокет с теми же свойствами, что и у исходного сокета, и возвращает описатель файла для него. После этого сервер может разветвить процесс или поток, чтобы обработать соединение для нового сокета и параллельно ожидать следующего соединения для оригинального сокета.
 
 
CONNECT–клиент запрашивает соединение (TCP).
 
 
SEND/SEND_TO – послать данные (TCP/UDP).
 
 
RECEIVE/RECEIVE_FROM – получить данные (TCP/UDP).
 
 
DISCONNECT – запросить разъединение (TCP).
 
  
 
===Постановка задачи===
 
===Постановка задачи===
Для ознакомления с сетевым программированием разберем пример.  
+
Для знакомства с сетевым программированием разберем пример. Пусть требуется запрограммировать службу удаленных вычислений. Клиенты просят сервер вычислить выражение (для начала содержащее только одну арифметическую операцию +, -, *, / ), сервер возвращает результат.
Пусть требуется запрограммировать службу удаленных вычислений. Клиенты просят сервер вычислить выражение (для начала содержащее только одну арифметическую операцию +, -, *, / ), сервер возвращает результат.
 
  
===Разработка протокола Calculation 0.1===
+
===Разработка протокола===
Назовем наш протокол Calculation 0.1.
+
Назовем наш протокол Calculation 0.1. Для начала определим формат запроса и ответа. Пусть запрос клиента должен начинаться со слова CALC, далее через пробел операция и потом два числа аргументы. Тогда запрос клиента на вычисление произведения 12 и 6 будет выглядеть так:
Для начала определим формат запроса и ответа.
 
Пусть, например, запрос клиента должен начинаться со слова CALC, далее через пробел оперция и потом два числа аргументы. Тогда запрос клиента на вычисление произведения 12*6 будет выглядеть так:
 
 
<pre>CALC * 12 6 ENTER</pre>
 
<pre>CALC * 12 6 ENTER</pre>
Ответы сервера будет начинаться со слова OK, если запрос был корректен и далее через пробел число – результат вычислений. Если же запрос был неправелен, ответом будет одно слово ERR.
+
Ответ сервера будет начинаться со слова OK, если запрос был корректен и далее через пробел число – результат вычислений. Если же запрос некорректен, ответом будет слово ERR.
<pre>ОК 72 ENTER (если все нормально)
+
<pre>ОК 72 ENTER (если все нормально)</pre>
ERR ENTER (если запрос неправильный)</pre>
+
<pre>ERR ENTER (если запрос неправильный)</pre>
 
 
 
ENTER нужен, чтобы узнать где конец строки.
 
ENTER нужен, чтобы узнать где конец строки.
  
После определения правил работы протокола, можно перейти к реализации. И тут встает вопрос: используем TCP или UDP?
+
После определения правил работы протокола, можно перейти к реализации. И тут встает вопрос: используем TCP или UDP? Мы разберем оба случая. И, для начала, разберем алгоритм взаимодействия сервера с клиентом, а потом реализуем его программно.
Мы разберем оба случая. И, для начала, разберем алгоритм взаимодействия сервера с клиентом, а потом попытаемся его реализовать программно на языке C++.
 
  
 
===Алгоритм работы сервера (TCP)===
 
===Алгоритм работы сервера (TCP)===
 
# Запускается заранее, до подключения клиентов.
 
# Запускается заранее, до подключения клиентов.
# Сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт № 12345.
+
# Сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт №12345.
 
# Выделяет память для очереди подключений.
 
# Выделяет память для очереди подключений.
 
# В цикле:
 
# В цикле:
Строка 70: Строка 32:
 
===Алгоритм работы сервера (UDP)===
 
===Алгоритм работы сервера (UDP)===
 
# Запускается заранее, до подключения клиентов.
 
# Запускается заранее, до подключения клиентов.
# сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт № 12345.
+
# сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт №12345.
 
# В цикле:
 
# В цикле:
 
::* ждет прихода сообщения;
 
::* ждет прихода сообщения;

Текущая версия на 02:48, 30 июля 2009

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

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

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

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

Назовем наш протокол 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();
        }
    }
}