tomcat请求处理分析(三) 绑定本地端口监听请求

1.1.1.1  bind方法

注意:这个bind可能在load的过程就已经加载,这里只是验证

   NioEndpoint就是使用Java中的NIO技术,来实行对Socket的处理。它主要包含两个部业务处理部分:Poller线程组和Acceptor线程组。

1.1.1.1.1     解析过程

   首先我们应该知道其bind方法做了一些什么操作,代码如下:

public void bind() throws Exception { // 打开监听信道 serverSock =ServerSocketChannel.open(); socketProperties.setProperties(serverSock.socket()); InetSocketAddress addr= (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort())); serverSock.socket().bind(addr,getBacklog()); serverSock.configureBlocking(true); //mimic APRbehavior serverSock.socket().setSoTimeout(getSocketProperties().getSoTimeout());

if (acceptorThreadCount==0) { // FIXME:Doesn't seem to work that well with multiple accept threads acceptorThreadCount = 1; } if (pollerThreadCount<=0) { //minimum one pollerthread pollerThreadCount = 1; } stopLatch = new CountDownLatch(pollerThreadCount); // Initialize SSL ifneeded if (isSSLEnabled()){         SSLUtil sslUtil = handler.getSslImplementation().getSSLUtil(this); sslContext =sslUtil.createSSLContext(); sslContext.init(wrap(sslUtil.getKeyManagers()), sslUtil.getTrustManagers(), null); SSLSessionContextsessionContext = sslContext.getServerSessionContext();         if (sessionContext != null) {             sslUtil.configureSessionContext(sessionContext); } // Determine which ciphersuites and protocols to enable enabledCiphers =sslUtil.getEnableableCiphers(sslContext); enabledProtocols =sslUtil.getEnableableProtocols(sslContext); } if (oomParachute>0)reclaimParachute(true); selectorPool.open(); }

1.1.1.1.1.1实例化ServerSocketChannelImpl

serverSock =ServerSocketChannel.open();

其方法具体实现:

public static ServerSocketChannel open() throws IOException{ return SelectorProvider.provider().openServerSocketChannel(); }

   在这个方法中进行了两步操作,第一步调用SelectorProvider的provider方法

public static SelectorProvider provider() { synchronized (lock) { if (provider != null) return provider;

        //在与当前线程相同访问控制权限的环境中,加载SelectorProvider实例  
        return AccessController.doPrivileged(
            new PrivilegedAction<SelectorProvider>() {
                public SelectorProvider run() {
                        if (loadProviderFromProperty())
                            return provider; //获取系统配置的SelectorProvider  
                        if (loadProviderAsService())
                            return provider; //获取类加载路径下的SelectorProvider  
                        //加载默认的SelectorProvider  
                        provider = sun.nio.ch.DefaultSelectorProvider.create();
                        return provider;
                    }
                });
    }

}

判断provider在当前进程是否已经被实例化过了,如果已经被实例化过了,那么就直接返回当前provider,不再执行后面的代码;否者就执行后面的代码实例化provider,

AccessController.doPrivileged()在与当前线程相同访问控制权限的环境中,加载SelectorProvider实例 

loadProviderFromProperty()这个函数判断如果系统属性java.nio.channels.spi.SelectorProvider 已经被定义了,则该属性名看作具体提供者类的完全限定名。加载并实例化该类;如果此进程失败,则抛出未指定的错误。

   loadProviderAsService()这个函数判断:如果在对系统类加载器可见的 jar 文件中安装了提供者类,并且该 jar 文件包含资源目录 META-INF/services 中名为java.nio.channels.spi.SelectorProvider 的提供者配置文件,则采用在该文件中指定的第一个类名称。加载并实例化该类;如果此进程失败,则抛出未指定的错误。

最后,如果未通过上述的方式制定任何provider,则实例化系统默认的provider并返回该结果(一般情况下,都是这种情况。)

这个地方需要注意的是:这里系统默认的provider在不同系统上是不一样的,下面用一个表格来表示:

系统

provider

MacOSX

KQueueSelectorProvider

Linux

Windows

WindowsSelectorProvider

进入sun.nio.ch.DefaultSelectorProvider.create(); 这里系统会根据不同的操作系统返回不同的provider;具体信息在上面的表格

总结:该方法的作用完成建立Pipe,并把pipe的读写文件描述符放入pollArray中,这个pollArray是Selector的枢纽

====================方法分界线=======================

上述是调用provider方法的具体过程,下面讲解一下调用其之后继续调用openServerSocketChannel的过程

以osx系统为例其返回了KQueueSelectorProvider,所以调用的方法是KQueueSelectorProvider.openServerSocketChannel

注意:其实这个方法不在KQueueSelectorProvider这个类中,而在其父类SelectorProviderImpl中,方法如下:

publicServerSocketChannelopenServerSocketChannel() throws IOException { return new ServerSocketChannelImpl(this);

}

即ServerSocketChannel.open()方法实际上是产生了一个子类ServerSocketChannelImpl的对象实例。其构造器如下:

ServerSocketChannelImpl(SelectorProvider var1) throws IOException {
    super(sp);
    this.fd = Net.serverSocket(true); //获取ServerSocket的文件描述符
    this.fdVal = IOUtil.fdVal(this.fd); //获取文件描述的id  
    this.state = ST_INUSE; //类变量 private static final int ST_INUSE = 0;

}

  所以在这里,serverSock = ServerSocketChannel.open();这个方法的作用是实例化ServerSocketChannelImpl,其成员变量具体实现代码如下:

//获取ServerSocket的文件描述符

class Net
{
   private static volatile boolean checkedIPv6 = false;
    private static volatile boolean isIPv6Available;
    public static final int SHUT_RD = 0;//关闭读操作
    public static final int SHUT_WR = 1;//关闭写操作
    public static final int SHUT_RDWR = 2;//关闭读写操作
    static 
    {
        //加载nio和net资源库
        Util.load();
        initIDs();
    }
    private static native void initIDs();
    //默认协议
    static final ProtocolFamily UNSPEC = new ProtocolFamily() {
    public String name()
    {
        return "UNSPEC";
    }

    };
    //获取ServerSocket文件描述
    static FileDescriptor serverSocket(boolean flag)
    {
        return IOUtil.newFD(socket0(isIPv6Available(), flag, true));
    }
    private static native int socket0(boolean flag, boolean flag1, boolean flag2);
}
=============================================================
class IOUtil
{
    static final int IOV_MAX = iovMax();
    static final boolean $assertionsDisabled = !sun/nio/ch/IOUtil.desiredAssertionStatus();
    static 
    {
        Util.load();
    }
    创建文件描述符
    static FileDescriptor newFD(int i)
    {
        FileDescriptor filedescriptor = new FileDescriptor();
        setfdVal(filedescriptor, i);
        return filedescriptor;
    }
}

//获取文件描述的id      

static native int fdVal(FileDescriptor filedescriptor);
1.1.1.1.1.2   构建socket并设置相关属性

socketProperties.setProperties(serverSock.socket());

serverSock.socket()的具体实现

publicServerSocket socket() {

    synchronized(stateLock) { // stateLock是一个new Object() 加载进行

        if(socket == null)

socket =ServerSocketAdaptor.create(this);

        returnsocket;

    }

}

============================create方法==============================

publicstatic ServerSocket create(ServerSocketChannelImpl ssc) {

           try {

 return new ServerSocketAdaptor(ssc);

           } catch (IOException x) {

               throw new Error(x);

           }

        }

==============================构造器=============================

private(ServerSocketChannelImpl ssc)

        throws IOException

    {

this.ssc = ssc;

}

====================ServerSocketChannelImpl类属性===============

    private final ServerSocketChannelImpl ssc;

private volatile int timeout = 0;

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

   此方法返回的是一个ServerSocket对象,其中利用同步保证了socket是一个单例

到了这里socketProperties.setProperties(serverSock.socket());这个方法就等价于socketProperties.setProperties(ServerSocket),其代码如下:

public void setProperties(ServerSocket socket) throws SocketException{ if (rxBufSize != null)         socket.setReceiveBufferSize(rxBufSize.intValue()); //设置输入流缓冲大小     if (performanceConnectionTime!=null&&performanceLatency!=null&& performanceBandwidth != null)         socket.setPerformancePreferences(//设置网络传输指标相对重要性 performanceConnectionTime.intValue(), performanceLatency.intValue(), performanceBandwidth.intValue());     if (soReuseAddress!=null)         socket.setReuseAddress(soReuseAddress.booleanValue());     if (soTimeout != null && soTimeout.intValue()>= 0)         socket.setSoTimeout(soTimeout.intValue()); }

总结:这段代码的作用是创建socket实例并给当前socket设置一些属性,包括输入流缓冲区、网络传输三项指标的相对重要性、端口是否可复用、设置读取超时时间,其实在启动过程中这些都是null,所以并没有进行什么设置

public int getReceiveBufferSize() throws SocketException

public void setReceiveBufferSize(int size) throwsSocketException

在默认情况下,输入流的接收缓冲区是8096个字节(8K)。这个值是Java所建议的输入缓冲区的大小。如果这个默认值不能满足要求,可以用setReceiveBufferSize方法来重新设置缓冲区的大小。但最好不要将输入缓冲区设得太小,否则会导致传输数据过于频繁,从而降低网络传输的效率。

如果底层的Socket实现不支持SO_RCVBUF选项,这两个方法将会抛出SocketException例外。必须将size设为正整数,否则setReceiveBufferSize方法将抛出IllegalArgumentException例外

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

public void setPerformancePreferences(int connectionTime,intlatency,int bandwidth)

以上方法的三个参数表示网络传输数据的三项指标:

参数connectionTime:表示用最少时间建立连接。

参数latency:表示最小延迟。

参数bandwidth:表示最高带宽。

setPerformancePreferences()方法用来设定这三项指标之间的相对重要性。可以为这些参数赋予任意的整数,这些整数之间的相对大小就决定了相应参数的相对重要性。例如,如果参数connectionTime为2,参数latency为1,而参数bandwidth为3,就表示最高带宽最重要,其次是最少连接时间,最后是最小延迟。

public boolean getReuseAddress() throws SocketException          

public void setReuseAddress(boolean on) throws SocketException

错误的说法:

通过这个选项,可以使多个Socket对象绑定在同一个端口上。

正确的说明是:

如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息,抛出“Addressalready in use: JVM_Bind”。如果你的服务程序停止后想立即重启,不等60秒,而新套接字依旧使用同一端口,此时SO_REUSEADDR 选项非常有用。必须意识到,此时任何非期望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不可能。

这个参数在Windows平台与Linux平台表现的特点不一样。在Windows平台表现的特点是不正确的,在Linux平台表现的特点是正确的。

在Windows平台,多个Socket新建立对象可以绑定在同一个端口上,这些新连接是非TIME_WAIT状态的。这样做并没有多大意义。

在Linux平台,只有TCP状态位于 TIME_WAIT ,才可以重用端口。这才是正确的行为。

使用SO_REUSEADDR选项时有两点需要注意:

    1.  必须在调用bind方法之前使用setReuseAddress方法来打开SO_REUSEADDR选项。因此,要想使用SO_REUSEADDR选项,就不能通过Socket类的构造方法来绑定端口。

    2.  必须将绑定同一个端口的所有的Socket对象的SO_REUSEADDR选项都打开才能起作用。如在例程4-12中,socket1和socket2都使用了setReuseAddress方法打开了各自的SO_REUSEADDR选项。

在Windows操作系统上运行上面的代码的运行结果如下:

这种结果是不正确的。

socket1.getReuseAddress():true

socket2.getReuseAddress():true

在Linux操作系统上运行上面的代码的运行结果如下:

这种结果是正确的。因为第一个连接不是TIME_WAIT状态的,第二个连接就不能使用8899端口;

只有第一个连接是TIME_WAIT状态的,第二个连接就才能使用8899端口;

public int getSoTimeout() throws SocketException

public void setSoTimeout(int timeout) throws SocketException

这个Socket选项在前面已经讨论过。可以通过这个选项来设置读取数据超时。当输入流的read方法被阻塞时,如果设置timeout(timeout的单位是毫秒),那么系统在等待了timeout毫秒后会抛出一个InterruptedIOException例外。在抛出例外后,输入流并未关闭,你可以继续通过read方法读取数据。

如果将timeout设为0,就意味着read将会无限等待下去,直到服务端程序关闭这个Socket.这也是timeout的默认值。如下面的语句将读取数据超时设为30秒:

1.1.1.1.1.3   创建套接字地址

InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));

创建套接字地址,并设置其端口

1.1.1.1.1.4   绑定地址和端口
serverSock.socket().bind(addr,getBacklog());
socket()是一个单例模式创建其实例,所以在这里还是上面的ServerSocketChannelImpl实例,然后调用其bind方法,方法代码如下:
public ServerSocketChannel bind(SocketAddress socketaddress, int i)
        throws IOException
    {
        synchronized(lock)
        {
            if(!isOpen())
                //如果socket关闭,则抛出ClosedChannelException
                throw new ClosedChannelException();
            if(isBound())
                 //如果已绑定,则抛出AlreadyBoundException
                throw new AlreadyBoundException();
             //确定inetsocketaddress
            InetSocketAddress inetsocketaddress = socketaddress != null ? Net.checkAddress(socketaddress) : new InetSocketAddress(0);
            SecurityManager securitymanager = System.getSecurityManager();
            if(securitymanager != null)
                 //检查地址端口监听权限
                securitymanager.checkListen(inetsocketaddress.getPort());
             //绑定前工作
            NetHooks.beforeTcpBind(fd, inetsocketaddress.getAddress(), inetsocketaddress.getPort());
             //实际地址绑定
            Net.bind(fd, inetsocketaddress.getAddress(), inetsocketaddress.getPort());
             //开启监听,如果参数i小于1,默认接受50个连接
            Net.listen(fd, i >= 1 ? i : 50);
            synchronized(stateLock)
            {
                 //更新ocalAddress
                localAddress = Net.localAddress(fd);
            }
        }
        return this;
    }

    从上面可以看出,bind首先检查ServerSocket是否关闭,是否绑定地址,如果既没有绑定也没关闭,则检查绑定的socketaddress是否正确或合法;然后通过Net工具类的bind(native)和listen(native),完成实际的

ServerSocket地址绑定和开启监听,如果绑定是开启的参数小于1,则默认接受50个连接。

1.1.1.1.1.5   serverSock设置成阻塞IO

serverSock.configureBlocking(true);

代码如下:

public finalSelectableChannelconfigureBlocking(boolean block) throws IOException { synchronized (regLock) { if (!isOpen()) throw new ClosedChannelException();         if (blocking == block) return this;         if (block && haveValidKeys()) throw new IllegalBlockingModeException(); implConfigureBlocking(block); blocking = block; } return this; }

1.1.1.1.1.6   设置读取超时时间

serverSock.socket().setSoTimeout(getSocketProperties().getSoTimeout());

1.1.1.1.1.7   初始化线程数
//初始化acceptor和poller线程数
if (acceptorThreadCount == 0) {
    // FIXME: Doesn't seem to work that well with multiple accept threads
    acceptorThreadCount = 1;
}
if (pollerThreadCount <= 0) {
    //minimum one poller thread
    pollerThreadCount = 1;
}
1.1.1.1.1.8   实例化线程同步辅助类
ountDownLatch类是一个同步计数器,构造时传入int参数,该参数就是计数器的初始值,每调用一次countDown()方法,计数器减1,计数器大于0 时,await()方法会阻塞程序继续执行
stopLatch = new CountDownLatch(pollerThreadCount);
参考链接:http://www.cnblogs.com/yezhenhan/archive/2012/01/07/2315652.html

   这个属性的作用是为了在关闭的时候确定所有的pollers关闭才继续向后执行

public void stopInternal() {     releaseConnectionLatch();     if (!paused) {         pause(); } if (running) { running = false; unlockAccept();         for (int i=0; pollers!=null &&i<pollers.length; i++) { if (pollers[i]==null) continue; pollers[i].destroy(); pollers[i] = null; } try { stopLatch.await(selectorTimeout+100, TimeUnit.MILLISECONDS); } catch (InterruptedExceptionignore) {         }         shutdownExecutor(); eventCache.clear(); nioChannels.clear(); processorCache.clear(); } }

1.1.1.1.1.9   NioSelectorPool实例设置属性

selectorPool.open();

 其中selectorPool是成员变量

private NioSelectorPool selectorPool = new NioSelectorPool();

在分析selectorPool.open();这段代码之前,我们必须了解Selector open()这个方法是干嘛,这个方法也在NioSelectorPool中

代码如下:

public staticSelector open() throws IOException {

      returnSelectorProvider.provider().openSelector();

}

   通过调用系统默认的SelectorProvider(这里不同的系统会有不同的SelectorProvider实现类)的openSelector()方法来创建新的selector

SelectorProvider.provider()这个方法我们已经在上文分析过,这里获取的就是同一个KQueueSelectorProvider实例

后面调用的也就是KQueueSelectorProvider.openSelector();源码如下:

public AbstractSelector openSelector()throws IOException {

    returnnew KQueueSelectorImpl(this);

}

根据代码可以看出其实例化了一个KQueueSelectorImpl,这是一个选择器,看一下选择器的作用,Selector选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。

所以下面代码的则用是构建blockingSelector实例,并将KQueueSelectorImpl给注入sharedSelector,这两个变量都是NioSelectorPool的属性

public void open() throws IOException{ enabled = true; getSharedSelector();     if (SHARED) { blockingSelector = new NioBlockingSelector(); blockingSelector.open(getSharedSelector()); } }

protected Selector getSharedSelector() throws IOException{ if (SHARED && SHARED_SELECTOR==null) { synchronized ( NioSelectorPool.class ) { if ( SHARED_SELECTOR==null)  { synchronized (Selector.class) { SHARED_SELECTOR=Selector.open(); } log.info("Usinga shared selector for servlet write/read"); }         }     } return  SHARED_SELECTOR; }

public static Selector open() throws IOException{ return SelectorProvider.provider().openSelector(); }

     下面这个方法是创建一个轮询线程,然后将选择器赋值给这个公司,并设置起为守护线程

public void open(Selector selector) { sharedSelector = selector; poller = new BlockPoller(); poller.selector = sharedSelector; poller.setDaemon(true); poller.setName("NioBlockingSelector.BlockPoller-"+(++threadCounter)); poller.start(); }

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏安富莱嵌入式技术分享

【RL-TCPnet网络教程】第19章 RL-TCPnet之BSD Socket服务器

本章节为大家讲解RL-TCPnet的BSD Socket,学习本章节前,务必要优先学习第18章的Socket基础知识。有了这些基础知识之后,再搞本章节会有事半功...

642
来自专栏熊二哥

Redis快速入门

由于工作慢慢从原来的少量用户的企业内部应用慢慢转化为了大量用户的企业内部应用或者直接转为了线上高并发应用,因而也渐渐的开始使用memcached、Redis等缓...

19610
来自专栏函数式编程语言及工具

ScalaPB(2): 在scala中用gRPC实现微服务

1443
来自专栏算法修养

Memcached 简单利用和简单了解(Mac的安装和使用)

Memcached 是一种用于分布式应用的一种缓存机制。应用也比较广泛。这里来学习一下。 首先Memcached 是分布式网站架构都需要用到的缓存机制。缓存就是...

3436
来自专栏蓝天

Tcpdump 的用法

更新时间:2005-12-26 11:55 阅读提示:第一种是关于类型的关键字,主要包括host,net,port, 例如 host 210.27.48.2,指...

954
来自专栏idba

系统调用跟踪分析神器--strace

最近遇到两起应用系统层面性能问题的案例,同事在排查问题的时候使用了strace这款神器,给自己在以后解决系统性能问题时提供了思路,本文学习了解系统分析工具---...

802
来自专栏逆向与安全

学习JVM虚拟机原理总结

1991年,在Sun公司工作期间,詹姆斯·高斯林和一群技术人员创建了一个名为Oak的项目,旨在开发运行于虚拟机的编程语言,允许程序多平台上运行。后来,这项工作就...

650
来自专栏安富莱嵌入式技术分享

【RL-TCPnet网络教程】第20章 RL-TCPnet之BSD Socket客户端

本章节为大家讲解RL-TCPnet的BSD Socket,学习本章节前,务必要优先学习第18章的Socket基础知识。有了这些基础知识之后,再搞本章节会有事半功...

722
来自专栏C/C++基础

Linux命令(9)——tcpdump命令

tcpdump是一款类Unix/Linux环境下的抓包工具,允许用户截获和显示发送或收到的网络数据包。tcpdump 是一个在BSD许可证下发布的自由软件。

753
来自专栏MasiMaro 的技术博文

PE文件详解(三)

在执行一个PE文件的时候,windows 并不在一开始就将整个文件读入内存的,二十采用与内存映射文件类似的机制。 也就是说,windows 装载器在装载的时...

713

扫描关注云+社区