Форум программистов
 

Восстановите пароль или Зарегистрируйтесь на форуме, о проблемах и с заказом рекламы пишите сюда - alarforum@yandex.ru, проверяйте папку спам!

Вернуться   Форум программистов > C/C++ программирование > C/C++ Сетевое программирование
Регистрация

Восстановить пароль

Купить рекламу на форуме - 42 тыс руб за месяц

Ответ
 
Опции темы Поиск в этой теме
Старый 01.11.2009, 19:30   #1
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию Статья: Низкоуровневое сетевое программирование. Пишем клиент/серверное приложение на сокетах Беркли

Вступление.
Итак. Свою новую статью я бы хотял посвятить низкоуровневому сетевому программированию. Постараюсь наиболее полно и одновременно сжато изложить основные принципы сетевого программирования, а так же будет рассмотрен пример построение рабочего многопоточного сервера и клиента. Все примеры разрабатывались и комплировались на Unix-подобной операционной системе и все ниже сказанное будет справедливо для любой Unix. Но, т.к. описываемое является стандартом, - данным материалом смогут воспользоваться и программисты, работающие в среде Windows (я на рассматривал конкретно сетевое программирование в этой ОС, т.к. не использую её), изменения коснуться, разве что, заголовочных файлов.
Как было сказано выше - будет рассмотрено низкоуровневое сетевое программирование. Справедливости ради, следует сказать, что оно, на самом деле, не такое уж низкоуровневое, т.к. существуют гораздо более низкие уровни, но все это, как правило, прерогатива ядра ОС/драйверов/железа. Для облегчения работы с сетью, операционной системой предоставляются особые объекты - сокеты (в некоторых книгах их называют "гнезда"), представляющие собой разновидность программных интерфейсов. Они позволяют представить сетевой интерфейс как простое устройство ввода/вывода и работать с ним, почти как с обычным файлом (что истинно, ибо в Unix все устройства представлены как файлы). Для работы с сокетами используются API, разработанные в Калифорнийском университете в городе Беркли (для BSD Unix) в 1983 году. Эти API являются сегодня стандартном де-факто и поддерживаются практически всеми современными операционными системами. Данный программный интерфейс, так же называют сокетами Беркли. В основе сокетов лежат протоколы TCP/IP и UDP. Рассмотрение особенностей каждого из них выходит за пределы данной статьи. Скажу только самое главное: TCP - это протокол, обеспечивающий надежное соединение и гарантированную доставку пакетов. UDP - протокол без установления соединения и без каких либо гарантий доставки пакета. IP - протокол сетевого уровня, служит транспортом для протоколов TCP и UDP.

От теории к действию.
Для работы с функциями сокетов необходимо подключить ряд заголовочных файлов, рассмотрим их:
<sys/socket.h>
Самый главный файл, в нем находятся базовые функции сокетов и структуры данных.
<netdb.h>
Функции для преобразования протокольных имен и имен хостов в числовые адреса.
<arpa/inet.h>
Функции для работы с числовыми IP-адресами.
<netinet/in.h>
Семейства адресов/протоколов PF_INET (для IPv4) и (PF_INET6 для IPv6). Включают в себя IP-адреса, а также номера портов TCP и UDP.
<netdb.h>
Функции для преобразования протокольных имен и имен хостов в числовые адреса.

Как было написано выше - сокеты схожи с файлами, их (сокеты) аналогично можно представить в виде числового дескриптора, а затем использовать этот дескриптор в стандартных функциях read и write. Для получения нового дескриптора сокета используется функция:
int socket(int domain, int type, int protocol);
Рассмотрим параметры:
int domain - этот параметр задает правила использования именования и формат адреса. Следует указывать PF_INET, если планируется работать с IPv4, либо PF_INET6 для IPv6.
int type - этот параметр задает тип сокета. Следует указывать SOCK_STREAM, если планируется использование протокола TCP, либо SOCK_DGRAM - в случае использования UDP.
int protocol - этот параметр указывает конкретный протокол, который следует использовать с данным сокетом. В качестве параметра следует использовать экземпляр структуры struct protoent. Ниже будет рассмотрено, как с помощью этой структуры и строк "tcp" или "udp" задать необходим протокол. Так же параметр может быть просто равен 0, тогда ядро само выберет соответствующий протокол.
Теперь посмотрим как это выглядит все вместе, написав небольшую функцию sock, которая, в дальнейшем, упростит нашу жизнь

Код:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

int sock(const char *transport)
{
	struct protoent    *ppe; 	//указатель на запись с информацией о протоколе
	int s, type;			//дескриптор и тип сокета
	
	//преобразовываем имя транспортного протокола в корректный номер протокола
	ppe = getprotobyname(transport);
	//определяем тип сокета, согласно имени транспортного протокола 	 
	if(strcmp(transport, "udp") == 0)
		type = SOCK_DGRAM;		//если udp - указываем SOCK_DGRAM
	else
		type = SOCK_STREAM;		//в ином (tcp) случае - указываем SOCK_STREAM
	//создаем новый сокет, передав необходимые параметры
	s = socket(PF_INET, type, ppe->p_proto);
	//возвращаем результат - дескриптор сокета.
	return s;
}

Последний раз редактировалось oleg kutkov; 01.11.2009 в 19:38.
oleg kutkov вне форума Ответить с цитированием
Старый 01.11.2009, 19:30   #2
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Данная функция может быть полезна как для разработки клиента, так и сервера, данный подход позволяет существенно сократить количество исходного кода и избежать дублирования кода. Здесь были использованы новая структура и функция: protoent - является удобным способом передачи параметров для функции сокета и работы с интернет-протоколами.
Функция sock возвращает дескриптор созданного сокета, либо же отрицательное значение в случае неудачи.
oleg kutkov вне форума Ответить с цитированием
Старый 01.11.2009, 19:32   #3
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Сервер.
Теперь рассмотрим построение полноценного рабочего сервера, на основе этой функции, отвечающего на запросы клиентов. Почему сервера, а не клиента ? Всегда следует начинать с разработки сервера, это удобнее, т.к. последний всегда можно проверить с помощью готового клиента, имеющегося в ОС - telnet, а клиента уже создавать на готовый сервер. Итак. Выше мы получили дескриптор сокета, что мы с ним должны сделать, что бы получился сервер ? Необходимо связать созданный сокет с определенным сетевым интерфейсом, на котором сервер будет "слушать" входящие подключения. Связывание выполняется с помощью функции bind, рассмотрим ее.
int bind(int sid, struct sockaddr* addr_p, int len);
Аргументы:
int sid - собственно сам дескриптор сокета.
struct sockaddr* addr_p - указатель на структуру адреса, с которым связывается сокет.
int len - длина структуры sockaddr

При успешном выполнении функция возвращает 0, при неудаче возвращает -1.
Перед тем, как рассматривать пример использования функции bind, познакомимся с еще одной важной функций - listen, а затем напишем полноценный код. Функция listen предназначена для "прослушивания" сетевого интерфейса, с которым связан серверный сокет. Т.е. она переводит сокет в режим ожидания входящих подключений.
Рассмотрим детально функцию listen.
int listen(int sid, int size);

Аргументы функции:
int sid - дескриптор сокета.
int size - максимальное число клиентов в очереди. Т.к. сокет не может обработать одновременно сразу все подключения - все запросы выстраиваются в очередь и ожидают своей обработки.

При успешном выполнении возвращается 0, при неуспешном возвращается -1.
Теперь рассмотрим применение функций bind и listen на небольшом примере.

Код:
#include <sys/types>
#include <sys/socket>

int listener(int sock, const char *host, const char *port)
{
	struct hostent     *phe; 	//указатель на запись с информацией о хосте
	struct sockaddr_in  sin; 	//структура IP-адреса
	
	//обнуляем структуру адреса
	memset(&sin, 0, sizeof(sin));
	//указываем тип адреса - IPv4, для IPv6 необходимо указать AF_INET6
	sin.sin_family = AF_INET;
	//задаем порт
	sin.sin_port = htons((unsigned short)atoi(port));
	//задаем адрес
	//преобразовываем строку адреса
	phe = gethostbyname(host);
	//копируем значение в поле структуры адреса
	memcpy(&sin.sin_addr, phe->h_addr, phe->h_length);
	//связываем сокет с указанным адресом, проверяя результат вызова функции
	if(bind(sock, (struct sockaddr *)&sin, sizeof(sin)) < 0)
		return -1; в случае неудачи возвращаем -1
	//включаем режим прослушивания для 5 клиентов, возвращая результат
	return listen(sock, 5);
}
oleg kutkov вне форума Ответить с цитированием
Старый 01.11.2009, 19:34   #4
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Итак, данная функция выполняет связывания сокета sock, с адресом host и портом port. Для преобразования host и port из строковых значений (например "192.168.1.0" и "21") в корректные бинарные значения используются функции gethostbyname и htons. Значения адреса и порта инициализируют поля структуры sockaddr_in, которая является параметром функции bind. Функция listener возвращает результат вызова функции listen.
На данно этапе мы научились создавать сокет, связывать его с сетевым интерфейсом и переключать сокет в режим прослушивания. Теперь осталось научится обрабатывать входящие подключения. В этом на поможет функция accept. Принцип работы очень прост: когда выполнение кода доходит до этой функции - выполнение останавливается. При входящем подключении выполнение кода продолжается и начинается процесс обмена данным с клиентом. Тут есть один важный момент - после успешного входящего подключения, функция accept возвращает новый дескриптор сокета. Над этим дескриптором и производятся операции чтения/записи, после чего этот дескритор закрывается с помощью функции close (по завершении работы сервера следует закрывать и сокет, созданный в функции sock, как и всякий файловый дескриптор). Для удобства цепочку "accept -> read/write -> close" заключают в бесконечный цикл. Для записи и чтения в сокет используются обычные функции write и read.
Рассмотрим функцию accept.
int accept(int sid, struct sockaddr* addr_p, int len_p);
Аргументы:
int sid - дескриптор сокета.
struct sockaddr - структура адреса, она инициализируется адресом подключившегося клиента
int len_p - размер структуры адреса

В случае удачи функция возвращает дескриптор нового сокета, в противном случае вовзвращает -1.

Подытожим, мы рассмотрели все функции, необходимые для построения полноценного сервера. Для большего понимания серверной архитектуры я нарисовал эту небольшую диаграмму.

Теперь самое время объеденить все вышеизученное и написать полный исходный код сервера, и протестировать его! Наш сервер будет принимать входящие подключения на всех доступных сетевых интерфейсах и при получении строки "hello" - будет отвечать строкой вида "hello, %computeradress% !!!", где %computeradress% - адрес удаленной машины.
Следует отметить, что в Unix, сервер, запущенный из под обычного пользователя, имеет право прослушивать на портах не менее 1024, для прослушивания на портах 0-1024 необходимы root права.
oleg kutkov вне форума Ответить с цитированием
Старый 01.11.2009, 19:35   #5
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Код сервера.
Код:
/************************************************/
/*	server.c - простой TCP/IP сервер	*/
/************************************************/

//подключаем необходимые заголовчные файлы
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <netdb.h>

extern errno; 	//глобальная переменная, которая хранит код последней ошибки

//проверка, задан ли шаблон INADDR_NONE, который обозначает сразу все доступные сетевые интерфейсы
//на некоторых платформах, он может быть не задан.
#ifndef INADDR_NONE
#define INADDR_NONE 0xfffffffff
#endif

//функция создания и связывания сокета. объявление
//аргументы:
//port - порт, с которым связывается сервер
//transport - протокол, по которому будет работать сервер (tcp или udp)
//qlen - длина  очереди
int sock(const char *port, const char *transport, int qlen);

//главная функция
int main()
{
	int msock, csock;				//дескрипторы сокетов
	struct sockaddr_in  remaddr;			//структура IP-адреса клиента
	unsigned int remaddrs = sizeof(remaddr);	//размер структуры адреса
	char msg[21];					//буфер сообщения
	
	msock = sock("1231", "tcp", 5);	//создаем tcp сокет и привязываем его к порту 3123, задав очередь 5
	if(msock < 0)			//проверяем значение дескриптора сокета
		return -1;		//завершаем программу

	while(1)	//бесконечный цикл
	{
		csock = accept(msock, (struct sockaddr*) &remaddr, &remaddrs);	//принимаем входящее подключение, адрес клиента в remaddr
		if(csock < 0)		//проверяем результат
			printf("Ошибка принятия подключения: %s\n", strerror(errno)); //сообщение об ошибке
		else			//если все нормально - начинаем обмен данными с клиентом
			{
				if(read(csock, &msg, sizeof(msg)) >0 )		//пробуем читать данные от клиента
				{
					if(strstr(msg, "hello"))			//если получено "hello"
					{
						memset(&msg, 0, sizeof(msg));			//обнуляем буфер
						strcpy(msg, "hello, ");				//формируем строку ответа
						strcat(msg, inet_ntoa(remaddr.sin_addr));	//преобразовываем адрес клиента в строку
						strcat(msg, " !!!\n\0");			//завершаем строку ответа
						write(csock, msg, sizeof(msg));			//отсылаем ответ
					}
				}
				close(csock);		//закрываем сокет клиента
			}
	}	
	close(msock);		//закрываем сокет сервера
	return 0;
}
oleg kutkov вне форума Ответить с цитированием
Старый 01.11.2009, 19:35   #6
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Продолжение.

Код:
//функция создания и связывания сокета. реализация
int sock(const char *port, const char *transport, int qlen)
{
	struct protoent *ppe;		
	struct sockaddr_in sin;
	int s, type;
	//обнуляем структуру адреса
	memset(&sin, 0, sizeof(sin));
	//указываем тип адреса - IPv4, для IPv6 необходимо указать AF_INET6
	sin.sin_family = AF_INET;
	//указываем, в качестве адреса, шаблон INADDR_ANY - все сетевые интерфейсы
	sin.sin_addr.s_addr = INADDR_ANY;
	//задаем порт
	sin.sin_port = htons((unsigned short)atoi(port));
	//преобразовываем имя транспортного протокола в номер протокола
	if((ppe = getprotobyname(transport)) == 0)
		{
			printf("Ошибка преобразования имени транспортного протокола: %s\n", strerror(errno));	//в случае неудачи выводим сообщение ошибки
			return -1;		
		}
	//используем имя протокола для определения типа сокета 	 
	if(strcmp(transport, "udp") == 0)
		type = SOCK_DGRAM;
	else
		type = SOCK_STREAM;	
	//создаем сокет
	s = socket(PF_INET, type, ppe->p_proto);
	if(s < 0)
		{
			printf("Ошибка создания сокета: %s\n", strerror(errno));	//в случае неудачи выводим сообщение ошибки
			return -1;
		}
	//привязка сокета с проверкой результата
	if(bind(s, (struct sockaddr *)&sin, sizeof(sin)) < 0)
		{
			printf("Ошибка связывания сокета: %s\n", strerror(errno));	//в случае неудачи выводим сообщение ошибки
			return -1;
		}
	//запуск прослушивания с проверкой результата
	if(type == SOCK_STREAM && listen(s, qlen) <0)
		{
			printf("Ошибка прослушивания сокета: %s\n", strerror(errno));	//в случае неудачи выводим сообщение ошибки
			return -1;
		}
	return s;	//возвращаем дескриптор сокета
}

Последний раз редактировалось oleg kutkov; 01.11.2009 в 19:44.
oleg kutkov вне форума Ответить с цитированием
Старый 01.11.2009, 19:36   #7
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Думаю, что еще какие либо пояснения для кода излишни. Комментарии подробно описывают все происходящее и для человека, внимательно прочитавшего всю информацию выше, нет здесь ничего непонятного. В данном коде отсутсвует функция listener - она объеденена с функцией sock, все остальное возложено на функцию main.
После компиляции (gcc server.c -o server) и запуска сервера (./server) можно пробовать подключаться к нему по telnet:

Как прекрасно видно - сервер отвечает на подключение к localhost:1231 и при получении строки "hello" - отвечает "hello, 127.0.0.1 !!!", а затем закрывает соединение.
Теперь пришла очеред разработать клиента для нашего сервера.
oleg kutkov вне форума Ответить с цитированием
Старый 01.11.2009, 19:37   #8
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Клиент.
Как было сказано выше - клиент имеет общую часть с сервером, а именно создание сокета. Но, в отличие от сервера, клиенту не нужно производить связывания сокета с адресами и переходить в режим прослушивания. Клиенту достаточно вызвать функцию connect, которая свяжет его сокет с удаленным сокетом сервера. Дальнейший процесс чтения/записи и закрытия соедиения сходен с таковыми у сервера.
Рассмотрим функцию connect подробно.
int connect(int sid, struct sockaddr* addr_p, int len);
Аргументы функции:
int sid - дескриптор сокета клиента.
struct sockaddr - структура адреса сервера, с которым необходимо соединится
int len - размер структур адреса.
Для наглядности рассмотрим, так же, диаграму работы клиента.

Теперь мы можем объеденить все вышеизученное и функцию connect для создания нашего клиента. Рассмотрим сразу полный исходный код.
oleg kutkov вне форума Ответить с цитированием
Старый 01.11.2009, 19:38   #9
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Код клиента.
Код:
/************************************************/
/*	client.c - простой TCP/IP клиент	*/
/************************************************/

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

extern errno; 	//глобальная переменная, которая хранит код последней ошибки

//функция подключения к серверу. объявление
//аргументы:
//host - адрес (имя) сервера
//port - порт сервера
//transport - протокол tcp или udp
int connectsock(const char *host, const char *port, const char *transport);

//главная функция
int main(int argc, char **argv)
{
	int sock; 	//сокет
	char msg[22];	//буфер сообщения

	if(argc == 3) 	//проверяем количество переданных аргументов.
	{
		//подключаем сокет, в качестве хоста - первый аргумент программы, в качестве порта - второй аргумент программы
		//напомню, что в argv[0] хранится имя самого исполняемого файла программы, поэтому его опускаем.
		sock = connectsock(argv[1], argv[2], "tcp");
		if(sock < 0)	//проверяем дескриптор сокета
			return -1;
		else 		//подключились
			{
				printf("Установлено соединение с %s:%s\n", argv[1], argv[2]);
				strcpy(msg, "hello\0");		//подготавливаем строку сообщения
				if(write(sock, msg, sizeof(msg)) < 0)		//отсылаем серверу
					{
						printf("Не удалось отправить данные серверу: %s\n", strerror(errno));
						return -1;
					}
				printf("Серверу отправлен \"hello\"\n");	//читаем ответ сервера	
				memset(&msg, 0, sizeof(msg));
				if(read(sock, msg, sizeof(msg)) < 0)
					{
						printf("Не удалось отправить данные серверу: %s\n", strerror(errno));
						return -1;
					}
				else				//выводим ответ сервера
					printf("От сервера получено: %s\n", msg);
			close(sock);	//закрываем сокет
			}

	} else	//иначе
		printf("Использование: server \"server\" \"port\"\n");	//выводим подсказку по использованию.
	return 0;
}
oleg kutkov вне форума Ответить с цитированием
Старый 01.11.2009, 19:39   #10
oleg kutkov
Unix C++ developer
Форумчанин
 
Аватар для oleg kutkov
 
Регистрация: 16.04.2007
Сообщений: 651
По умолчанию

Продолжение.

Код:
//функция подключения к серверу. реализация
int connectsock(const char *host, const char *port, const char *transport)
{
	struct hostent *phe; //указатель на запись с информацией о хосте
	struct servent *pse; //указатель на запись с информацией о службе
	struct protoent *ppe; //указатель на запись с информацией о протоколе
	struct sockaddr_in sin; //структура IP-адреса оконечной точки 
	int s, type; //дескриптор сокета и тип сокета

	//обнуляем структуру адреса
	memset(&sin, 0, sizeof(sin));
	//указываем тип адреса (IPv4) 
	sin.sin_family = AF_INET;
	//задаем порт
	sin.sin_port = htons((unsigned short)atoi(port)); 	
	//преобразовываем имя хоста в IP-адрес, предусмотрев возможность представить его
	//в точечном десятичном формате
	if(phe = gethostbyname(host))
		memcpy(&sin.sin_addr, phe->h_addr, phe->h_length);
	//преобразовываем имя транспортного протокола в номер протокола
	if((ppe = getprotobyname(transport)) == 0)
		{
			printf("Ошибка преобразования имени транспортного протокола: %s\n", strerror(errno));	//в случае неудачи выводим сообщение ошибки
			return -1;			
		}	
	//используем имя протокола для определения типа сокета 	 
	if(strcmp(transport, "udp") == 0)
		type = SOCK_DGRAM;
	else
		type = SOCK_STREAM;			
	//создание сокета
	s = socket(PF_INET, type, ppe->p_proto);
	if(s < 0)
		{
			printf("Ошибка создания сокета: %s\n", strerror(errno));	//в случае неудачи выводим сообщение ошибки
			return -1;
		}
	//попытка подключить сокет
	if(connect(s, (struct sockaddr *)&sin, sizeof(sin)) < 0)
		{
			printf("Не удалось подключится к серверу: %s\n", strerror(errno));	//в случае неудачи выводим сообщение ошибки
			return -1;			
		}
	//возвращаем дескриптор подключенного сокета
	return s;
}

Последний раз редактировалось oleg kutkov; 01.11.2009 в 19:51.
oleg kutkov вне форума Ответить с цитированием
Ответ


Купить рекламу на форуме - 42 тыс руб за месяц



Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Простейшее клиент-серверное приложение в Delphi 2009 RNT Работа с сетью в Delphi 15 21.11.2010 19:08
Возможно-ли Клиент-серверное приложение типа Клиент(Pascal) а сервер(CGI)? Demol Работа с сетью в Delphi 1 21.04.2009 16:18
Язык низкоуровневое программирование Assembler jackpatriot Assembler - Ассемблер (FASM, MASM, WASM, NASM, GoASM, Gas, RosAsm, HLA) и не рекомендуем TASM 1 03.01.2009 19:05
Клиент-серверное приложение: Callback MaTBeu Общие вопросы C/C++ 13 02.06.2008 20:27
Клиент-серверное приложение veryseldom Работа с сетью в Delphi 8 20.08.2007 19:57