Как работать с сырыми сокетами (SOCK_RAW) | Часть 1

Как работать с сырыми сокетами (SOCK_RAW) | Часть 1 World of Tanks

Мультиплексирование,
основанное на переключении в jdk 1.4

Когда вы читаете из сокета или пишете в него, вам нужно сделать
передачу данных рациональной. Давайте рассмотрим сначала операцию записи. Когда вы
пишите данные на уровне приложения (TCP или UDP сокет), вы пишите данные в рабочий
буфер системы.

Эти данные, в конечном счете, формируют (TCP или UDP) пакеты, которые
необходимо передать на машину назначения по сети. Когда вы пишите в сокет и, если в
буфере нет достаточно доступного места, запись может блокироваться. Если вы читаете
из сокета и нет достаточного количества информации для чтения из буфера операционной
системы, куда попадают данные после получения из сети, чтение будет блокировано.

Если
есть нить (поток) для операции чтения или записи, эта нить не может делать ничего и
может стать причиной снижения произовдительности вашей программы. До появления JDK 1.4
не было способа вывести такую нить из заблокированного состояния. С помощью каналов вы
можете выполнить асинхронную операцию закрытия на канале и нить, блокированная на этом
канале примет AsynchronousCloseException.

Асинхронный ввод-вывод в Java достигается тем же способом, который
дает вызов метода select( ) в UNIX подобных системах. Вы можете дать список дескрипторов
(чтения или записи) в функцию select( ) и она отследит этот дескриптор на возникновение
некоторых событий.

Для дескриптора, представляющего сокет, из которого вы читаете, данные в
буфере операционной системы для этого буфера представляются событием. Для дескрипторов,
представляющих сокеты, в которые вы пишите, наличие места для записи во внутреннем
буфере операционной системы для этого сокета представляется событием. Поэтому вызов
метода select( ) исследует различные дескрипторы для проверки событий.

Что, если вы просто читаете и пишите в дескриптор когда бы вы не
захотели? Select может обрабатывать множество дескрипторов, что позволит вам мониторить
множество сокетов. Рассмотрим пример чат-сервера, когда сервер имеет соединения с
различными клиентами.

Тип данных, достигающих сервера, перемежается. Сервер предназначен
для чтения данных из сокета и отображения их в GUI, то есть для показа каждому клиенту
— чтобы достич этого, вы читаете данные от каджого клиента и пишите эти данные всем
остальным клиентам.

Например 5 клиентов: 1, 2, 3, 4 и 5. Если сервер запрограммирован
на выполнение чтения от 1 и записи в 2, 3, 4 и 5, затем происходит чтения от 2 и запись
в 1, 3, 4, 5 и так далее, то может так случиться, что пока нить сервера заблокирована
на чтении одного из клиентских сокетов, могут появиться данные на других сокетах.

Одно из решений состоит в том, чтобы создавать различные нити для кадого клиента (до
JDK1.4). Но это не масштабируемое решение. Вместо этого вы можете иметь селектор,
основанный на механизме, следящем за всеми клиентскими сокетами. Он знает какой сокет
имеет данные для чтения без блокирования.

Но если единственная нить выполняет эту
работу (выбор и запись каждому клиенту) он не будет хорошо откликаться. Таким
образом в таких ситуациях одна нить мониторит сокеты на чтение, выбирает сокет, из
которого можно осуществить чтение, и делегирует остальную ответственность (запись
другим клиентам) другой нити (нитям) или пулу нитей.

Этот шаблон называется шаблоном реактора, когда события
отсоединяются от действия, ассоциированного с событиями (Pattern Oriented
Software Architecture — Doug Schmidt).

В JDK 1.4 вы создаете канал, регестрируете объект Селектора
в канале, который (объект) будет следить за событиями в канале. Многие каналы
регестрируют один и тот же объект Селектора. Единственная нить, которая
вызывает Selector.select(), наблюдает множество каналов.

Каждый из классов
ServerSocket, Socket и DatagramSocket имеют метод getChannel( ), но он возвращает
null за исключением того случая, когда канал создается с помощью вызова метода
open( ) (DatagramChannel.open( ), SocketChannel.open( ), ServerSocketChannel.open( )).
Вам необходимо ассоциировать сокет с этим каналом.

Вы мультиплексируете несколько каналов (то есть сокеты), используя
Селектор. Статический вызов Selector.select( ) блокирует выполнение до возникновения
события в одном из каналов. Существует так же и не блокирующая версия этого метода,
которая принимает количество милисекунд для засыпания или блокирования до того
момента, когда вызов метода завершится.

ByteBuffer используется для копирования данных из канала и в канал.
ByteBuffer является потоком октетов и вы декодируете этот поток, как символы. Со
стороны клиента в MultiJabberClient.java это выполняется путем использования классов
Writer’а и OutputStreamWriter’а. Эти классы конвертируют символы в поток байтов.

Про WoT:  Обзор King Tiger (захваченный) - Twitch Prime набор Echo

0x2. создание

=============

Перво-наперво. Создание. Как создается сырой сокет? Какие основные в хитросплетениях? Необработанный сокет создается путем вызова системного вызова socket (2) и определив тип сокета как SOCK_RAW следующим образом:

	int fd = socket(AF_INET, SOCK_RAW, XXX);

где XXX — это * протокол int *, который, как мы обсудим далее, является главный источник путаницы и проблем, вникающий в сам факт того, что здесь могут применяться разные комбинации. Допустимые значения: IPPROTO_RAW, IPPROTO_ICMP, IPPOROTO_IGMP, IPPROTO_TCP, 0 (осторожно — см. Ниже)

Прежде чем переходить к конкретным комбинациям для каждой ОС, давайте сначала рассмотрим посмотрите на фактическое значение значения * протокола *. Чтобы все протоколы работали одновременно был использован конкретный общий подход к проектированию. Согласно ему, похожие протоколы сгруппированы в домены.

PF_INET / AF_INET 			--> Internet protocols (TCP, UDP etc)
PF_LOCAL, PF_UNIX / AF_LOCAL, AF_UNIX	--> Unix local IPC protocol
PF_ROUTE / AF_ROUTE    --> routing tables
Linux определяет эти константы в /usr/src/linux-2.6.*/include/linux/socket.h 

/* Поддерживаемые семейства адресов. */
#define AF_UNSPEC	0
#define AF_UNIX		1	/* Unix domain sockets 		*/
#define AF_LOCAL	1	/* POSIX name for AF_UNIX	*/
#define AF_INET		2	/* Internet IP Protocol 	*/
  	/* ... */
/* Семейства протоколов, такие же, как семейства адресов. */
#define PF_UNSPEC	AF_UNSPEC
#define PF_UNIX		AF_UNIX
#define PF_LOCAL	AF_LOCAL
#define PF_INET		AF_INET
 	/* ... */

FreeBSD определяет указанные выше значения (почти такие же) в /usr/src/sys/sys/socket.h Как вы уже догадались, мы займемся Семья AF_INET. Семейство Интернета разбивает свои протоколы на протокол типы, каждый из которых может состоять более чем из одного протокол.

Linux определяет типы протоколов семейства Интернет в /usr/src/linux-2.6.*/include/linux/net.h

enum sock_type {
	SOCK_STREAM	= 1,
	SOCK_DGRAM	= 2,
	SOCK_RAW	= 3,
	SOCK_RDM	= 4,
	SOCK_SEQPACKET	= 5,
	SOCK_DCCP	= 6,
	SOCK_PACKET	= 10,
};
FreeBSD определяет типы AF_INET в 

/usr/src/sys/sys/socket.h 

/*
 * Types
 */
#define	SOCK_STREAM	1		/* stream socket */
#define	SOCK_DGRAM	2		/* datagram socket */
#define	SOCK_RAW	3		/* raw-protocol interface */
#if __BSD_VISIBLE
#define	SOCK_RDM	4		/* reliably-delivered message */
#endif
#define	SOCK_SEQPACKET	5		/* sequenced packet stream */

Если вы в прошлом занимались программированием сокетов, то вы, вероятно, признать некоторые из вышеперечисленных. Один из них должен быть вторым аргументом вызов socket (AF_INET, …, …). Третий аргумент — это значение IPPROTO_XXX, которое определяет фактический протокол над IP.

0                   1                   2                   3   
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
   |Version|  IHL  |Type of Service|          Total Length         |
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
   |         Identification        |Flags|      Fragment Offset    |
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
   |  Time to Live |    Protocol   |         Header Checksum       |
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
   |                       Source Address                          |
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
   |                    Destination Address                        |
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
   |                    Options                    |    Padding    |
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 

Это одно из самых важных полей, поскольку именно оно будет использоваться уровень IP на стороне получателя, чтобы понять, какой уровень находится над ним (для пример TCP или UDP) дейтаграмма должна быть доставлена.

Linux определяет эти протоколы в /usr/src/linux-2.6.*/include/linux/in.h

Обслуживание множества клиентов

JabberServer работает, но он может обработать только одного клиента
одновременно. В обычных серверах вы захотите, чтобы была возможность иметь дело со многими
клиентами одновременно. Ответом является многопоточность, и в языках, которые не поддерживают
многопоточность напрямую, это означает что вы встретите все возможные трудности.

В
Главе 14 вы
видели, что многопоточность в Java проста насколько это возможно, учитывая это, можно сказать,
что многопоточность весьма сложная тема. Поскольку нити (потоки) в Java достаточно
прамолинейны, то создание сервера, который обрабатывает несколько клиентов, относительно
простое заняте.

Основная схема состоит в создании единственного ServerSocket’а на
сервере и вызове метода accept( ) для ожидания новых соединений. Когда accept( ) возвращается,
вы получаете результирующий сокет и используете его для создания новой нити (потока),
работа которой будет состоять в ослуживании определенного клиента. Затем вы вызовите метод
accept( ) снова, чтобы подождать нового клиента.

В следующем коде сервера вы можете видеть, что он очень похож на
пример JabberServer.java, за исключением того, что все операции по обслуживанию определенного
клиента былы помещены внутрь отдельного thread-класса:

Нить ServeOneJabber принимает объект Socket’а, который
производится методом accept( ) в main( ) при каждом новом соединении с клиентом.
Затем, как и прежде, с помощью Socket, создается BufferedReader и PrintWriter
с возможностью автоматического выталкивания буфера.

Ответственность за очистку сокета должна быть, опять таки, внимательно
спланирована. В этом случае, сокет создается вне ServeOneJabber, так что ответственность
может быть совместная. Если конструктор ServeOneJabber завершится неудачей, он просто
выбросит исключение тому, кто его вызвал, и кто должен очистить нить.

Обратите внимание на упрощенность MultiJabberServer. Как и прежде создается
ServerSocket и вызывается метод accept( ), чтобы позволить новое соединение. Но в это время
возвращаемое значение метода accept( ) (сокет) передается в конструктор для ServeOneJabber,
который создает новую нить для обработки этого соединения. Когда соединение завершиется,
нить просто умирает.

Про WoT:  Рабочий оленемер (мод XVM) - последняя версия оленеметра для World of tanks 1.17.0.1 WOT

Если создание ServerSocket’а проваливается, то из метода main( ), как и прежде,
выбрасывается исключение. Но если создание завершается успешно, внешний блок try-finally
гарантирует очистку. Внутренний try-catch гарантирует только от сбоев в конструкторе
ServeOneJabber. Если конструктор завершится успешно, то нить ServeOneJabber закроет
соответствующий сокет.

Для проверки этого сервера, который реально обрабатывает несколько
клиентов, приведенная ниже программа создает несколько клиентов (используя нити),
которые соединяются с одним и тем же сервером. Максимальное допустимое число нитей
определяется переменной final int MAX_THREADS.

Конструктор JabberClientThread принимает InetAddress и использует
его для открытия сокета. Вероятно, вы заметили шаблон: сокет всегда используется для
создания определенного рода объектов Reader’а и Writer’а (или InputStream и/или
OutputStream), которые являются тем единственным путем, которым может быть использован
сокет.

(Вы можете, конечно, написать класс или два для автоматизации этого процесса
вместо набора этого текста, если вас это беспокоит.) Далее, start( ) выполняет инициализацию
нити и запуск run( ). Здесь сообщение посылается на сервер, а информация с сервера отображается
на экране.

Однако, нить имеет ограниченноен время жизни и, в конечном счете, завершается.
Обратите внимание, что сокет очищается, если конструктор завершился неудачей после
создания сокета, но перед тем, как конструктор завершится. В противном случае, ответственность
за вызов close( ) для сокета ложиться на метод run( ).

Threadcount хранит информацию о том, сколько в настоящее время существует
объектов JabberClientThread. Эта переменная инкрементируется, как часть конструктора и
декрементируется при выходе из метода run( ) (что означает, что нить умерла). В методе
MultiJabberClient.main( ) вы можете видеть, что количество нитей проверяется, и если их
много, то нить более не создается.

Простейший
сервер и клиент

Этот пример покажет простейшее использование серверного и клиентского
сокета. Все, что делает сервер, это ожидает соединения, затем использует сокет,
полученный при соединении, для создания InputStream’а и OutputStream’а. Они конвертируются
в Reader и Writer, которые оборачиваются в BufferedReader и PrintWriter.

Клиент создает соединение с сервером, затем создает OutputStream и
создает некоторую обертку, как и в сервере. Строки текста посылаются через полученный
PrintWriter. Клиент также создает InputStream (опять таки, с соответствующей конвертацией
и оберткой), чтобы слушать, что говорит сервер (который, в данном случае, просто отсылает
слова назад).

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

Вот сервер:

Вы можете видеть, что для ServerSocket’а необходим только номер порта,
а не IP адрес (так как он запускается на локальной машине!). Когда вы вызываете accept( ),
метод блокирует выполнение до тех пор, пока клиент не попробует подсоединится к серверу.

Здесь тщательно обработана отвественность за очистку сокета. Если
конструктор ServerSocket завершится неудачей, программа просто звершится (обратите
внимание, что мы должны предположить, что конструктор ServerSocket не оставляет никаких
открытых сокетов, если он зваершается неудачей).

По этой причине main( ) выбрасывает
IOException, так что в блоке try нет необходимости. Если конструктор ServerSocket
завершится успешно, то все вызовы методов должны быть помещены в блок try-finally,
чтобы убедиться, что блок не будет покинут ни при каких условиях и ServerSocket будет
правильно закрыт.

Аналогичная логика используется для сокета, возвращаемого из метода
accept( ). Если метод accept( ) завершится неудачей, то мы должны предположить, что
сокет не существует и не удерживает никаких ресурсов, так что он не нуждается в
очистке. Однако если он закончится успешно, то следующие выражения должны быть помещены
в блок try-finally, чтобы при каких-либо ошибках все равно произошла очистка.

И ServerSocket и Socket, производимый методом accept( ), печатаются в
System.out. Это означает, что автоматически вызывается их метод toString( ). Вот что он
выдаст:

ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]Socket[addr=127.0.0.

Короче говоря, вы увидите как это соответствует тому, что делает
клиент.

Следующая часть программы выглядит, как открытие файла для
чтения и записи за исключением того, что InputStream и OutputStream создаются
из объекта Socket. И объект InputStream’а и OutputStream’а конвертируются в объекты
Reader’а и Writer’а с помощью «классов-конвертеров» InputStreamReader и
OutputStreamreader, соответственно.

Про WoT:  Cougar Ultimus RGB World of Tanks (CGR-WT2MB-WTK) – купить клавиатуру, сравнение цен интернет-магазинов: фото, характеристики, описание | E-Katalog

Вы можете также использовать классы из Java 1.0
InputStream и OutoutStream напрямую, но, с точки зрения вывода, есть явное
преимущество в использовании этого подхода. Оно проявляется в PrintWriter’е,
который имеет перегруженный конструктор, принимающий в качестве второго аргумента
флаг типа boolean, указывающий, нужно ли автоматическое выталкивание буфера вывода в
конце каждого выражения println( ) (но не print( )).

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

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

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

Обратите внимание, что фактически все открытые вами потоки,
буфферезированы. В конце этой главы есть упражнение, которое покажет вам, что
происходит, если вы не буфферезируете потоки (вещи становятся медленнее).

Когда клиент посылает строку, содержащую «END»,
программа прекращает цикл и закрывает сокет.

Вот клиент:

В main( ) вы можете видеть все три способа получение InetAddress
IP адреса локальной заглушки: с помощью null, localhost или путем явного указания
зарезервированного адреса 127.0.0.1, если вы хотите соединится с машиной по сети,
вы замените это IP адресом машины. Когда печатается InetAddress (с помощью автоматического
вызова метода toString( )), то получается результат:

При передачи в getByName( ) значения null, он по умолчанию
ищет localhos и затем производит специальныйы адрес 127.0.0.1.

Обратите внимание, что Socket создается при указании и InetAddress’а,
и номера порта. Чтобы понять, что это значит, когда будете печатать один из объектов
Socket помните, что Интернет соединение уникально определяется четырьмя параметрами:
клиентским хостом, клиентским номером порта, серверным хостом и серверным номером порта.

Когда запускается сервер, он получает назначаемый порт (8080) на localhost (127.0.0.1).
Когда запускается клиент, он располагается на следующем доступном порту на своей
машине, 1077 — в данном случае, который так же оказался на той же самой машине
(127.0.0.1), что и сервер.

Теперь, чтобы передать данные между клиентом и сервером,
каждая сторона знает, куда посылать их. Поэтому, в процессе соединения с
«известным» сервером клиент посылает «обратный адрес», чтобы
сервер знал, куда посылать данные. Вот что вы видите среди выводимого стороной сервера:

Socket[addr=127.0.0.1,port=1077,localport=8080]

Это означает, что сервер просто принимает соединение с адреса
127.0.0.1 и порта 1077 во время прослушивания локального порта (8080). На клиентской стороне:

Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]

Это значит, что клиент установил соединение с адресом
127.0.0.1 по порту 8080, используя локальный порт 1077.

Вы заметите, что при каждом повторном запуске клиента
номер локального порта увеличивается. Он начинается с 1025 (первый после
зарезервированного блока портов) и будет увеличиваться до тех пор, пока вы
не перезапустите машину, в таком случае он снова начнется с 1025.

Как только объект Socket будет создан, процесс перейдет к
BufferedReader и PrintWriter, как мы это уже видели в сервере (опять таки, в
обоих случаях вы начинаете с Socket’а). В данном случае, клиент инициирует
обмен путем посылки строки «howdy», за которой следует число.

Обратите внимание, что буфер должен опять выталкиваться (что происходит
автоматически из-за второго аргумента в конструкторе PrintWriter’а). Если
буфер не будет выталкиваться, процесс обмена повиснет, поскольку начальное
«howdy» никогда не будет послана (буфер недостаточно заполнен, чтобы
отсылка произошла автоматически).

Вы можете видеть, что аналогичные меры приняты, чтобы
быть уверенным в том, что сетевые ресурсы, представляемые сокетом, будут
правильно очищены. Для этого используется блок try-finally.

Сокет производит «посвященную» связь, которая
остается постоянной до тех пор, пока не будет явного рассоединения. (Посвященная
связь может быть рассоединена неявно, если одна из сторон, или посредническая
связь соединения рушатся.) Это означает, что два партнера замкнуты в коммуникации
и соединение постоянно открыто.

Оцените статью
TankMod's
Добавить комментарий