Недавно изучая сетевое программирование на Java и знания, связанные с Netty, я узнал, что Netty — это сетевая структура режима NIO, но предоставляет различныеChannel
Для поддержки различных режимов обработки сетевых коммуникаций, включая синхронный, асинхронный, блокирующий и неблокирующий. Обучение начинается с основ, поэтому мы должны сначала понять соответствующие базовые концепции и собственный NIO Java. Здесь я подытожу то, что я недавно узнал для вашего понимания.
В целях экономии вашего времени основное содержание этой статьи выглядит следующим образом:
- Асинхронная, блокирующая концепция
- Тип ввода/вывода ОС
- Базовая реализация Java NIO
Асинхронный, синхронный, блокирующий, неблокирующий
Синхронный и асинхронный фокус на механизмах передачи сообщений, так называемая синхронизация означает, что после того, как вызывающая сторона сделает вызов, вызов не вернется до тех пор, пока не будет получен результат, но как только вызов вернется, будет получено возвращаемое значение.Синхронизация означает, что вызывающая сторона активно ожидает результата вызова.;Наоборот, асинхронно, он возвращается сразу после выполнения вызова, поэтому возвращаемого значения может не быть.Когда есть возвращаемое значение, вызываемый уведомит вызывающего через состояние и уведомление.Асинхронный означает, что вызываемый объект уведомляет вызывающего о том, что результат вызова готов.*. *Следовательно, они имеют разные механизмы передачи сообщений: один для того, чтобы вызывающая сторона проверяла, готов ли результат вызова, а другая — для того, чтобы вызываемая сторона уведомляла вызывающую сторону о том, что результат готов.
Блокирующий и неблокирующий фокус на состоянии программы при ожидании результата вызова (сообщение, возвращаемое значение). Блокирующий вызов означает, что текущий поток будет приостановлен до того, как будет возвращен результат вызова, а вызывающий поток продолжит выполнение только после получения результата. Неблокирующий вызов означает, что вызывающий поток не будет приостановлен до тех пор, пока структура не будет немедленно доступна или пока он не сможет выполнять другие действия.
Существуют четыре ситуации, когда два набора понятий объединяются друг с другом, а именно: синхронная блокировка, синхронная неблокировка, асинхронная блокировка и асинхронная неблокировка. Давайте возьмем пример для сравнения четырех случаев апелляции по отдельности.
Например, если вы хотите скачать файл 1G из Интернета, после нажатия кнопки загрузки, если вы находились рядом с компьютером и ждали окончания загрузки, эта ситуация блокирует синхронизацию, если вам не нужно оставаться рядом компьютер все время, можно зайти в книгу я знаю, но вы все равно время от времени проверяете ход загрузки, эта ситуация синхронная неблокирующая, если вы были рядом с компьютером, но загрузчик будет играть музыку чтобы напомнить вам после завершения загрузки, это асинхронная блокировка; но если вы не сидите за компьютером, читаете книгу, а загрузчик играет музыку, чтобы напомнить вам, когда загрузка закончилась, то ситуация асинхронная и не -блокировка.
Типы ввода/вывода Unix
Зная приведенные выше два набора концепций, давайте рассмотрим 5 моделей ввода-вывода, доступных в Unix:
- Блокирующий ввод-вывод (блокирующий ввод-вывод)
- Неблокирующий ввод-вывод (неблокирующий ввод-вывод)
- Мультиплексирование ввода-вывода (мультиплексирование ввода-вывода)
- Ввод-вывод, управляемый сигналом (ввод-вывод, управляемый сигналом)
- Асинхронный ввод-вывод (асинхронный ввод-вывод)
Первые четыре являются синхронными, только последний является асинхронным вводом-выводом.Следует отметить, что ***Java NIO полагается на мультиплексирующий ввод-вывод системы Unix.Для операций ввода-вывода это синхронный ввод-вывод , но для модели программирования это асинхронный сетевой вызов*** Ниже мы будем использовать системуread
вызов, чтобы представить различные типы ввода/вывода.
бытьread
Когда это происходит, оно проходит две стадии:
- 1 Дождитесь подготовки данных
- 2 Скопируйте данные из памяти ядра в память процесса
Различные типы ввода-вывода ведут себя по-разному на этих двух этапах. Однако, поскольку в этом разделе много контента, и большая его часть представляет собой описательные знания, здесь мы даем только несколько иллюстраций для объяснения, и заинтересованные студенты могут пойти, чтобы узнать больше об этом.
Базовая реализация Java NIO
Все мы знаем, что Netty предоставляет Native Socket Transport через JNI, почемуNetty
Как насчет предоставления собственной версии NIO? Очевидно, что нижний уровень Java NIO также основан наepoll
Звоните (последняя версия). Здесь не будем сначала об этом, давайте подумаем о возможных ситуациях. Следующий исходный код взят из версии OpenJDK-8u40-b25.
открытый метод
Если мы будем следоватьSelector.open()
Легко найти метод по классу за классомSelector
инициализируетсяDefaultSelectorProvider
генерируются в соответствии с различными платформами операционных системSelectorProvider
, для систем Linux генерируетEPollSelectorProvider
экземпляр, и этот экземпляр будет генерироватьEPollSelectorImpl
как финалSelector
выполнить.
class EPollSelectorImpl extends SelectorImpl
{
.....
// The poll object
EPollArrayWrapper pollWrapper;
.....
EPollSelectorImpl(SelectorProvider sp) throws IOException {
.....
pollWrapper = new EPollArrayWrapper();
pollWrapper.initInterrupt(fd0, fd1);
.....
}
.....
}
EpollArrayWapper
Системные вызовы Linux, связанные с epoll, инкапсулированы в собственные методы дляEpollSelectorImpl
использовать.
private native int epollCreate();
private native void epollCtl(int epfd, int opcode, int fd, int events);
private native int epollWait(long pollAddress, int numfds, long timeout,
int epfd) throws IOException;
Приведенные выше три нативных метода соответствуют трем системным вызовам, связанным с epoll в Linux.
//创建一个epoll句柄,size是这个监听的数目的最大值.
int epoll_create(int size);
//事件注册函数,告诉内核epoll监听什么类型的事件,参数是感兴趣的事件类型,回调和监听的fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待事件的产生,类似于select调用,events参数用来从内核得到事件的集合
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
Итак, мы найдем вEpollArrayWapper
вызывается в конструктореepollCreate
метод, который создает дескриптор для epoll. так,Selector
Даже если объект создан.
метод регистрации
иopen
аналогичный,ServerSocketChannel
изregister
Нижний слой функции называетсяSelectorImpl
Категорияregister
метод, этоSelectorImpl
то естьEPollSelectorImpl
родительский класс.
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
//生成SelectorKey来存储到hashmap中,一共之后获取
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
//attach用户想要存储的对象
k.attach(attachment);
//调用子类的implRegister方法
synchronized (publicKeys) {
implRegister(k);
}
//设置关注的option
k.interestOps(ops);
return k;
}
EpollSelectorImpl
Соответствующий метод реализован следующим образом, который вызываетEPollArrayWrapper
изadd
метод, запишите значение fd, соответствующее каналу, а затем добавьте лыжу вkeys
в переменной. существуетEPollArrayWrapper
Есть массив байтовeventLow
Запишите значения fd всех каналов.
protected void implRegister(SelectionKeyImpl ski) {
if (closed)
throw new ClosedSelectorException();
SelChImpl ch = ski.channel;
//获取Channel所对应的fd,因为在linux下socket会被当作一个文件,也会有fd
int fd = Integer.valueOf(ch.getFDVal());
fdToKey.put(fd, ski);
//调用pollWrapper的add方法,将channel的fd添加到监控列表中
pollWrapper.add(fd);
//保存到HashSet中,keys是SelectorImpl的成员变量
keys.add(ski);
}
Мы найдем это призваниеregister
метод не предполагаетEpollArrayWrapper
родной метод вepollCtl
, потому что они откладывают вызов этого метода до тех пор, покаSelect
в методе.
Выберите метод
иregister
метод аналогичен,SelectorImpl
серединаselect
метод заканчивает тем, что вызывает свой подклассEpollSelectorImpl
изdoSelect
метод
protected int doSelect(long timeout) throws IOException {
.....
try {
....
//调用了poll方法,底层调用了native的epollCtl和epollWait方法
pollWrapper.poll(timeout);
} finally {
....
}
....
//更新selectedKeys,为之后的selectedKeys函数做准备
int numKeysUpdated = updateSelectedKeys();
....
return numKeysUpdated;
}
Из приведенного выше кода видно, чтоEPollSelectorImpl
сначала позвониEPollArrayWapper
изpoll
метод, затем обновитеSelectedKeys
. вpoll
метод будет вызываться первымepollCtl
предварительно зарегистрироваться наregister
Fd канала, сохраненный в методе и интересующем типе события, затемepollWait
Метод ожидает генерации интересующего события, что приводит к блокировке потока.
int poll(long timeout) throws IOException {
updateRegistrations(); ////先调用epollCtl,更新关注的事件类型
////导致阻塞,等待事件产生
updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
.....
return updated;
}
После ожидания наступления интересующего события (или после того, как время ожидания превысит заданное максимальное время),epollWait
Функция вернется.select
Функция возобновляет работу из заблокированного состояния.
метод selectedKeys
Давайте сначала посмотримSelectorImpl
серединаselectedKeys
метод.
//是通过Util.ungrowableSet生成的,不能添加,只能减少
private Set<SelectionKey> publicSelectedKeys;
public Set<SelectionKey> selectedKeys() {
....
return publicSelectedKeys;
}
Странно, зачем сразу возвращатьсяpublicSelectedKeys
, этоselect
Была ли эта переменная изменена во время выполнения функции?
publicSelectedKeys
Этот объект на самом делеselectedKeys
Копия переменной, вы можете найти ее вSelectorImpl
Найдите связь между ними в конструкторе , давайте оглянемся назадselect
серединаupdateSelectedKeys
метод.
private int updateSelectedKeys() {
//更新了的keys的个数,或在说是产生的事件的个数
int entries = pollWrapper.updated;
int numKeysUpdated = 0;
for (int i=0; i<entries; i++) {
//对应的channel的fd
int nextFD = pollWrapper.getDescriptor(i);
//通过fd找到对应的SelectionKey
SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
if (ski != null) {
int rOps = pollWrapper.getEventOps(i);
//更新selectedKey变量,并通知响应的channel来做响应的处理
if (selectedKeys.contains(ski)) {
if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
numKeysUpdated++;
}
} else {
ski.channel.translateAndSetReadyOps(rOps, ski);
if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
selectedKeys.add(ski);
numKeysUpdated++;
}
}
}
}
return numKeysUpdated;
}
постскриптум
Увидев это, все уже подробно разобрались в базовой реализации NIO.Здесь я хочу поговорить о двух вопросах.
Во-первых, почему Netty повторно реализовала базовые методы NIO, связанные с нативной стороной? Послушайте, что должен сказать основатель Netty.Ссылка на сайт. Поскольку версия Java использует режим epoll, запускаемый уровнем, в то время как Netty хочет использовать режим, запускаемый границей, а версия Java не предоставляет некоторые элементы конфигурации epoll, такие как TCP_CORK и SO_REUSEPORT.
Второй - просмотреть столько исходников, какой смысл тратить столько времени? Я чувствую, что если я смотрю на это с неутилитарной точки зрения, то мне просто хочется узнать больше. Иногда после прочтения исходный код или понимание лежащих в его основе принципов, будет использовать чувство внезапного осознания, такое какAQS
Принцип. Если вы посмотрите на это с точки зрения цели, то после того, как вы узнаете основной принцип, ваша уверенность будет сильнее. Если есть проблема, вы можете быстрее ее обнаружить и решить. Кроме того, вы также можете используйте исходный код в качестве шаблона для создания собственных колес в соответствии с конкретной реальностью и реализуйте версию, которая больше соответствует вашим текущим потребностям.
Если в будущем будет время, я надеюсь хорошо понять принцип реализации epoll на уровне операционной системы.