socket编程实例_NIO之网络编程源码阅读

 2023-09-09 阅读 20 评论 0

摘要:之前分析了ByteBuffer、Channel相关的基本知识,现在对于NIO的基石已经有了基本的了解。不过NIO最突出的特性还是其基于select编程模型的网络编程体验。NIO网络编程通常有两种使用方式,同步阻塞式编程(与BIO网络编程相同)和同步非阻塞(sele

之前分析了ByteBuffer、Channel相关的基本知识,现在对于NIO的基石已经有了基本的了解。不过NIO最突出的特性还是其基于select编程模型的网络编程体验。

NIO网络编程通常有两种使用方式,同步阻塞式编程(与BIO网络编程相同)和同步非阻塞(select模型)。下面就从NIO的编程模型来了解NIO的基本原理。

一、NIO阻塞式网络编程

NIO可以像传统的Socket编程一样,进行阻塞同步的Socket编程,样例代码如下:

socket编程步骤?服务端

e43b662a94f9f7dc693aea2bd9837963.png

客户端

4f4f58a9d91647ca6521c1f438bfa8a6.png

socket编程例子,socket通信基本流程如下

06fdbb1120c12c62f84829c3e6fc5171.png

结合socket通信的基本流程和样例代码进行初步分析

ServerSocketChannel

结合样例代码,可以看到NIO中的xxxSocketChannel对象其实就是对应着系统层面的socket,服务端通过下面方法创建了一个服务端的server—socket套接字用来监听本地端口,从而能够响应客户端的connect事件,从而建立服务端与客户端的tcp连接

socket编程 c语言、

e02c88fdb3443efab34e86652190e557.png

默认情况下的provider应该是下面这个类:

a4645845ec0d249a4b597dabc9fabda0.png

在Linux环境中实际创建的SelectorProvider为EPollSelectorProvider

socket网络编程教程、

e410f03b5143c0a1a86b0e1ed6f44e2d.png

默认情况下EPollSelectorProvider的实例是通过反射进行创建的

f7491415c5597a73b1ddafdeb891deae.png

openServerSocketChannel方法的具体实现在EpollSelectorProvider的父类SelectorProviderImpl中提供具体实现:

网络编程socket,

4ee3bdac9f668c62c3aabab3c506da84.png

openServerSocketChannel方法生成的ServerSocketChannel实例的实际类型为ServerSocketChannelImpl,

4f5be3fa8adb54ccd189ebeddfbab893.png

fd是通过Net类创建的socket所对应的文件描述符

socket套接字编程。

3f19ce9c8144d88125a9fe4676195573.png

参数true表明这是一个stream类型的套接字,即tcp套接字

ecb2957ee580391add91bd8d71787ec7.png

socket0是一个native方法

socket代码,

0466a9833481f4782a83b6dc21923c77.png

如果当前支持ipv6则创建ipv6的套接字,如果不支持则创建ipv4的套接字,SOCKET_STRAM表明是tcp套接字,SOCK_DGRAM表明是UDP套接字

创建socket套接字是使用的c语言的socket库函数,其内部通过socket系统调用创建了一个socket套接字对应的文件描述符,

socket系统调用​blog.csdn.net

13c965d8bb5b811d277b178ebf1e25d9.png

c语言socket编程udp,通过上述源码可以了解到ServerSocketChannel是一个实际类型为ServerSocketChannelImpl的实例,该实例的fd属性关联了一个socket文件描述符

服务端的套接字创建后,需要先绑定到本地的具体地址,然后监听本地端口,即socket通信模型中的bind和listen两个步骤:

4b58a4f7f2da18dda49bdc924c6bbeb1.png

在NIO中,bind和listen在同一个方法中完成,backlog参数是服务端半连接队列的大小

c++ socket编程。

837b1dc977f8b8f7d5f2ff26afd8c46b.png

服务端开始listen本地端口之后,可以调用accept方法,同步阻塞的接收客户端的连接请求

77869a04907b2ff3232c1db0e8be8991.png

f6ac18be1f88a874ba8c6ef69641f4dd.png

socket网络编程基础。accept也是个系统调用,所以要依靠native方法来实现

1a5113eef64051102e0df709312dff59.png

native调用了accept系统调用,当有新的客户端与服务端成功建立tcp连接后,则会为这个tcp连接创建一个socket文件描述符

0b288f378e3ff8a636ff6d53c46510b4.png

configureBlocking方法给新建立的客户端与服务端之间的socket对应的文件描述符设置了阻塞标志,该动作是通过fcntl系统调用来完成的:

fcntl​blog.csdn.net

84508998b0c00ca3b945e6fc1b3f433e.png

SocketChannel:

Java中将已建立的tcp连接封装成SocketChannel对象,服务端的SocketChannel是通过监听本地绑定的端口然后通过accept操作建立的,而客户端的SocketChannel是通过主动与服务端进行connect建立的:

SocketChannel的open方法,可以向服务端发起一个连接建立请求:

c3537865e37d7762f9493a70b72bfda5.png

和ServerSocketChannel一样,客户端的SocketChannel也是通过SelectorProvider创建的

120d1b7d0d5ce8dfb1eb3d0f65b42f19.png

94cc1ef95f1b884fd3c2e568fb1299a5.png

与ServerSocketChannel不同的是,客户端socket对应的文件描述符是通过Net.socket方法创建的

17f1717d25b546bb07848475b19aab46.png

c5c0ae414cb7adac3344fadf63602c77.png

其底层同样是通过socket0这个native方法实现的,不过与ServerSocketChannel创建不同的是其reuse参数为false

ecaafd0d41868ce551f9efdab7a90bf0.png

当reuse为true时,会通过setsockopt给socket设置SO_REUSEADDR这个参数,表示可以进行端口复用。通常客户端是不允许进行端口复用的,而服务端一般来说可能有好几张网卡,所以可以考虑给每一个网卡上的IP地址都绑定相同的监听端口来启动多个服务端进程。

5994b16e191c429fd26baa305432197e.png

当客户端的SocketChannel实例创建完成后,通过调用connect向服务端发送连接建立请求:

59485ad527cb875a7fa28bc5eeedda6d.png

568a3a4a33de93851ab4949c99f39de2.png

Net.connect通过调用connect0这个native方法完成连接建立请求的发送

1381c1860dc982a61e62383eb6cfb7c3.png

connect0的底层实现为connect系统调用

connect系统调用​blog.csdn.net

9169d8ceaccd61b22dfe95dca3874fcf.png

当连接建立完成后,就可以通过socket套接字进行数据的读写的。由于NIO中直接面向的数据对象为Buffer,所以一般开发中要借助ByteBuffer来完成数据的读写功能,至于Buffer的基本概念,可以看前面的源码分析:

landexiang:NIO之Buffer​zhuanlan.zhihu.com
dc266dd9eb10d6a994ce47fafa190c99.png

二、NIO同步非阻塞式网络编程

NIO同步阻塞的网络编程模型和传统BIO网络编程模型并没有什么太大不同,唯一的优点就是通过操作堆外内存进行数据传输效率高一些,但是这并没有显著的改善BIO的痛点,即BIO是阻塞的,所以NIO支持了同步非阻塞的多路复用网络编程模型。

NIO服务端

42d1a7e96cea8b2eac17fbd3f698d192.png

26b5633c253b72b268ec1a1bc6ee4746.png

a4f6a13f7dee3a06dcf68b93ad2b8dc7.png

NIO客户端

2ad24231da22cb926e0befaa10093012.png

46e13fc2b031abb72ce75895d2534ea9.png

7c3869b2a253f84155f32ee8813f59cb.png

从上面的demo来看,NIO非阻塞式编程主要有以下几个特点:

  • 无论是ServerSocketChannel还是SocketChannel,都需要显示的设置为非阻塞模式
  • 开发者通过注册事件的方式来表明当前SocketChannel/ServerSocketChannel下一次要进行什么动作
  • SocketChannel/ServerSocketChannel是否已经可以执行开发者注册的事件所对应的操作,交给Selector来管理,对开发者是透明的
  • 每个Channel在同一个Selector上同一时间只能存在一个事件,如果想在同一个Channel上执行多个操作可以有两种办法,创建多个Selector或者是每次处理完一个事件后注册另外一个事件

下面来通过源码稍微深入的了解一下Selector是如何工作的

Selector

Selector是通过open方法创建的

884c7a24e705abcb6aff8f28a2fa2aac.png

从前面的分析可知,默认的provider是EPollSelectorProvider,所以Selector的实际类型为EPollSelectorImpl

0e0ba749c4596c3f8f9e57f61c2a72a1.png

bd1e7453ebef9aea9ab6a283710718b8.png

EPollSelectorImpl实例中首先通过pipe系统调用创建了一个管道

linux 管道​blog.csdn.net

管道创建后,fd数组中保存了读端和写端的文件描述符,并且都被置位非阻塞的模式

9857e3233e2ea8e1c26b51883ebbdc90.png

EPollArrayWrapper是epoll的文件描述符的包装类

8c83d9562c61f543bb7f390d31bc9ca1.png

其内部epfd属性保存的是通过epoll_create系统调用创建的epoll的文件描述符

19e43a439a791526e7afc8999ac6b353.png

操作系统层面的epoll创建成功之后,将之前创建的管道的输入端和输出端对应的文件描述符分别保存起来,然后通过epollCtl函数,将管道的输出端对应的文件描述符添加到epoll中,注册事件类型为EPOLLIN,即有数据到达时epoll会认为其处于就绪状态

b3d0cc714763a819a11b9ff58984c314.png

epollCtl底层是通过epoll_ctl系统调用完成的

epoll​blog.csdn.net

8af28cd95bd907c4398bd34f064b217d.png

register 注册事件

在Selector创建完成之后,需要将Channel注册到Selector上,该方法由ServerSocketChannel和SocketChannel的公共抽象父类SelectableChannel提供

19cb7d4846965770d9b919063e04e2d8.png

其内部调用了抽象子类AbstractSelectableChannel的register方法,从源码来看,如果不讲Channel设置为非阻塞的模式,在注册事件时会直接抛异常

8bb574d310dcaba5311f0a859c63f4f6.png

findKey方法会先遍历当前Channel中所有的SelectionKey,找出已经在当前Selector上已经注册过的事件 如果没找到则说明是第一次注册

d8e2cada4e9971734d5a2dc42aa19d3f.png

f2934125b2551d9aae93b5679297796c.png

如果不是第一次注册,说明是重复注册,则需要对之前的事件进行覆盖,SelectionKey.interestOps方法提供修改epoll监听事件的功能

8a3d1589876840eec80ea5f1f50ab483.png

dcd6b0fe049dc4f86d57c5882ba2f6a5.png

以SocketChannelImpl和EpollSelectorImpl为例,在SocketChannelImpl的translateAndSetInterestOps方法中,将SelectKey添加到了EpollSelectorImpl中

d67c32c2b53c895d2ba2836e2503db2e.png

pollWrapper前面已经介绍过,是epoll的java层面的包装类,

95b436e249150404720c2e620e966685.png

setInterest方法首先保存了SocketChannel持有的文件描述符

c4a2c239d02b33b5b594513b074590bd.png

通过注释可以得知eventsLow和eventsHigh是两个等待变更epoll监听事件的缓冲区,即在Channel已经注册过事件的Selector重新注册事件时,事件的并不会实时的更新到操作系统层面的epoll中,而是先将变更事件在内存中缓存起来

94070a2dc798e2568867a84a3033ff8a.png

41213d66e3570b314e2f0c080ba16366.png

上述是重新注册事件的情况,下面来看下首次注册事件的流程。首次注册的任务是交给Selector的register方法完成的

825900c8bcee593bb4bc76368cc4077e.png

源码中告诉我们 SelectionKey中保存了Channel和Selector的引用,以便于在处理事件时进行引用

07e1e2589ca18184150d9b4b8536d3ec.png

而至于interestOps方法,前面在介绍重新注册时已经分析过,该事件并没有同步的注册到操作系统层面的epoll中

最后在首次注册完事件后,Channel需要将该事件保存到自己的数组中

5e767f5d976b04035d532568b6fee079.png

Selector.select 取出就绪事件

当注册完Channel对应的事件后,调用Selector的select方法即可进行阻塞式的等待,当Selector中任何一个Channel的任何一个事件到达时,阻塞解除。select方法的具体工作是交给子类重写的doSelect方法来完成的

6ede0f99753af09ddfa8092bcb60ed6c.png

以Linux操作系统环境下的EPollSelectorImpl为例

cd700cb3cd18b46685891bb05b4d1271.png

processDeregisterQueue是对已经取消的SelectionKey的相关数据清理工作

77c4a63b0bef5c7c719d7c050cb49028.png

7c47263f49faa219591393deb6d7f8e7.png

c8b12ade4669a8b2197e18ac5a69161d.png

清理完已经无效的SelectionKey之后,通过poll(timeout)方法触发epoll的select动作,不过在select之前首先要将上一次select到这次select之间新添加或者新修改的文件描述符的注册事件生效到同步到操作系统层面的epoll上去

7a37f59b5575389568f36ae30e428467.png

同步注册事件的动作依然依赖于epoll_ctl系统调用

19800395aa071ffb85f942a07f3dfdab.png

epollwait是等待就绪事件到达的主要动作,从源码看其底层有两种方式,有限等待和无限等待,默认情况下不显示指定过期时间就是无线等待,即下面的epoll_wait,而不是iepoll,epoll_wait同样是一个系统调用

933a00dcc1ac4fa904e6cfc219a94daf.png

当epoll_wait系统调用完成后,如果有就绪的事件到达,这些事件和对应的文件描述符已经被放置到内存中一块连续的地址空间中去了,然后需要比对当前Selecotor中所有的SelectionKey,将所有就绪的SelectionKey取出来放到selectedKeys集合中:

updated是当前Selector中所有的事件数量,在进行一次select之后,所有的就绪事件都被保存在了selectedKeys集合中

4940d4389e2678750348d936fea7c4d8.png

所以我们可以通过Selector.selectedKeys()方法取出所有的就绪事件进行相应

上面就是select的基本过程,无非就是给socket对应的文件描述符注册事件,然后用epoll去监听是否有事件到达。不过我们需要注意一个问题,epoll可以是无限阻塞的,假如我们的线程由于其他原因被中断了,我们如何去显示的唤醒底层的epoll呢?否则他将一直阻塞下去。这里就要来看下Java是怎么做的。

线程中断异常处理——主动唤醒epoll

回顾一下前面的内容,EPollSelectorImpl实例中通过IOUtil.makePipe创建了一个管道,并且把管道的输入端(读端)、输出端(写端)的文件描述符传递给了epoll的包装类EPollArrayWrapper,并且在EPollArrayWrapper中给管道的端文件描述符注册了可读事件监听:

4ef1b3e218af9be8e633fed5eb058ac8.png

而管道的输出端,也就是写端在interrupt方法中被调用

004bcf5087e777f7496caea7abad1f07.png

该方法的主要作用就是向管道写入一个字节的数据

5262b2df5219bed4210109c0e71c07db.png

由于管道的输入端(读端)已经在epoll中注册了EPOLLIN事件,假如当前epoll处于阻塞状态,则会监听到这个读事件从而唤醒线程

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/3/30894.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息