Tomcat原理系列之七:详解socket如何封装成request(下)

简介: Tomcat原理系列之七:详解socket如何封装成request(下)

@TOC

推荐阅读Tomcat原理系列之二:由点到线,请求主干对于理解本文有很多帮助。

Tomcat版本8.


1. 接收连接:


Accptor在接受到socket请求后,执行setSocketOptions方法对socket进行初步的封装。 封装: 首先创建一个SocketBufferHandler用于socket输入输出的缓冲(SocketBuffer)。将SocketBufferHandler与socket一同封装成NioChannel.

public SocketBufferHandler(int readBufferSize, int writeBufferSize,
            boolean direct) {
        this.direct = direct;
        if (direct) {
            readBuffer = ByteBuffer.allocateDirect(readBufferSize);//默认8k
            writeBuffer = ByteBuffer.allocateDirect(writeBufferSize);
        } else {
            readBuffer = ByteBuffer.allocate(readBufferSize);
            writeBuffer = ByteBuffer.allocate(writeBufferSize);
        }
    }


2. 注册:


调用Poller.register()将NioChannel(socket)先进一步封装成NioSocketWrapper类,再封装成PollerEvent然后注册到Poller的events队列中去。


3. 消费:


Poller.run()消费队列的PollerEvent事件。将PollerEvent中准备就绪的socketChannel注册到Selector。


4. 处理请求:


Poler.run() 从Selector选择处就绪的Channel。调用NioEndpoint.processKey(),processKey()方法中,根据读写事件调用processSocket()处理。


5. Worker线程:


processSocket()会根据(NioSocketWrapper)socket创建一个SocketProcessor处理器。SocketProcessor本身实现了Runnable接口。可以作为任务。被Endpoint的Executor线程池执行。

try {
            if (socketWrapper == null) {
                return false;
            }
            SocketProcessorBase<S> sc = processorCache.pop();
            if (sc == null) {
                sc = createSocketProcessor(socketWrapper, event);
            } else {
                sc.reset(socketWrapper, event);
            }
            Executor executor = getExecutor();//线程池
            if (dispatch && executor != null) {
                executor.execute(sc);
            } else {
                sc.run();
            }
        } catch (RejectedExecutionException ree) {

SocketProcessor在连接握手成功的情况下,调用ConnectionHandler.process()方法开始socket内容的读取


6. HTTP1.1协议处理器初始化:


ConnectionHandler.process()方法会创建Http11Processor处理器用于http协议的处理. Http11Processor构造方法主要做了,

  • 首先会创建一对org.apache.coyote.Request和org.apache.coyote.Response内部coyoteRequest与coyoteResponse对象.
  • 并创建Http11InputBuffer与Http11OutputBuffer用于coyoteRequest与coyoteResponse。Http11InputBuffer提供HTTP请求头的解析与编码功能。Http11InputBuffer在创建的时候会指定headerBufferSize的大小.默认也是8k.


7. Http11Processor.service()[HTTP协议头部的解析]:


拿到Http11Processor后.执行核心方法service();第一步:初始化读写缓冲区

// Setting up the I/O
        setSocketWrapper(socketWrapper);
        inputBuffer.init(socketWrapper);
        outputBuffer.init(socketWrapper);

init()方法为Http11InputBuffer内部创建一个读缓冲区byteBuffer.大小为headerBufferSize+socketbuffer的大小.也就是默认是2*8k

void init(SocketWrapperBase<?> socketWrapper) {
        wrapper = socketWrapper;
        wrapper.setAppReadBufHandler(this);
        int bufLength = headerBufferSize +
                wrapper.getSocketBufferHandler().getReadBuffer().capacity();
        if (byteBuffer == null || byteBuffer.capacity() < bufLength) {
            byteBuffer = ByteBuffer.allocate(bufLength);
            byteBuffer.position(0).limit(0);
        }
    }

第二步开始请求行的解析在解析之前我先来看看HTTP请求报文格式.


image.png


inputBuffer.parseRequestLine()方法用来读取请求行。inputBuffer中有个parsingRequestLinePhase属性值.parsingRequestLinePhase值不同代表读取请求行的不同位置.

  • 0:表示解析开始前跳过空行
  • 2: 开始解析请求方法: POST
  • 3: 跳过请求方法和请求uri之间的空格或制表符
  • 4: 开始解析请求URI: chapter17/user.html
  • 5:跳过请求URI与版本之间的空格
  • 6:解析协议版本: HTTP/1.1

parseRequestLine()方法, 每读一个位置时,都会判断inputBuffer.bytebuffer中是否读取完毕。position = limit 即已经读完了,需要执行fill重新填充,参数是false表示非阻塞读(那什么时候阻塞读呢,是我们在调用getInputStream()时,是阻塞的)

// Read new bytes if needed
if (byteBuffer.position() >= byteBuffer.limit()) {//判断
   if (keptAlive) {
       // Haven't read any request data yet so use the keep-alive
       // timeout.
       wrapper.setReadTimeout(wrapper.getEndpoint().getKeepAliveTimeout());
   }
   if (!fill(false)) {//填充
       // A read is pending, so no longer in initial state
       parsingRequestLinePhase = 1;
       return false;
   }
   // At least one byte of the request has been received.
   // Switch to the socket timeout.
   wrapper.setReadTimeout(wrapper.getEndpoint().getConnectionTimeout());
}

fill()填充方法:填充buffer fill()的填充功能是通过调用socket的包装类NioSocketWrapper.read()方法实现的. 在read()方法中 首先会尝试从socketBufferHandler.readbuffer读,

  • 如果socketBufferHandler.readbuffer有数据,把数据填充到inputBuffer.bytebuffer中。不需要从socket通道读取。
  • 如果socketBufferHandler.readbuffer没有数据可读,且inputBuffer.bytebuffer的可写空间大于socketBufferHandler.readbuffer的容量: 则直接从socket通道中读取。设置该次读取的最大值limit,为socket buffer的大小
  • 如果socketBufferHandler.readbuffer没有数据可读,且inputBuffer.bytebuffer的可写空间小于socketBufferHandler.readbuffer的容量:则先从socket通道读入socketBuffer(因为此时socketBuffer的容量大于inputBuffer.bytebuffer的可写空间,可以一次从OS读取更多数据)。然后再从socketBuffer填充到inputBuffer.bytebuffer.(此时填充的是剩余可写空间,这样socketBuffer也会剩余一些,当inputBuffer.bytebuffer读取完毕时,再调用fill()方法时,将剩余socketBuffer的数据填充到inputBuffer.bytebuffer,不需要去socket通道内读,本质上时减少OSread.然后这样循环执行下去,直到所有的读操作完成)
@Override
        public int read(boolean block, ByteBuffer to) throws IOException {
          //先从tomcat 底层socket buffer 缓冲区读,如果buffer缓冲区还有未读的buffer,则不需要到OS底层读缓冲区读
            int nRead = populateReadBuffer(to);
            if (nRead > 0) {
                return nRead;
                /*
                 * Since more bytes may have arrived since the buffer was last
                 * filled, it is an option at this point to perform a
                 * non-blocking read. However correctly handling the case if
                 * that read returns end of stream adds complexity. Therefore,
                 * at the moment, the preference is for simplicity.
                 */
            }
            // The socket read buffer capacity is socket.appReadBufSize
            int limit = socketBufferHandler.getReadBuffer().capacity();
            if (to.remaining() >= limit) {
                to.limit(to.position() + limit);
                nRead = fillReadBuffer(block, to);
                if (log.isDebugEnabled()) {
                    log.debug("Socket: [" + this + "], Read direct from socket: [" + nRead + "]");
                }
                updateLastRead();
            } else {
                // Fill the read buffer as best we can.
                nRead = fillReadBuffer(block);
                if (log.isDebugEnabled()) {
                    log.debug("Socket: [" + this + "], Read into buffer: [" + nRead + "]");
                }
                updateLastRead();
                // Fill as much of the remaining byte array as possible with the
                // data that was just read
                if (nRead > 0) {
                    nRead = populateReadBuffer(to);
                }
            }
            return nRead;
        }

read()调用fillReadBuffer()方法来完成从socket通道内读数据。fillReadBuffer有两种读模式阻塞读和非阻塞读.非阻塞读会调用socket的初始包装类NioChannel.read()方法,NioChannel.read()调用SocketChannel.read()此处是真正从通道里读数据.

总结起来说。填充功能其实从socket通道把数据读到inputBuffer.byteBuffer中

解析:inputBuffer从byteBuffer中解析报文内容.例如请求方法,请求URI。inputBuffer并没有把字节转义。而是使用byte[]数组的包装类MessageBytes来表示请求行的各部分,在需要的时候进行转移并缓冲。

我们以请求方法读取为例:

if (parsingRequestLinePhase == 2) {
            //
            // Reading the method name
            // Method name is a token
            //
            boolean space = false;
            while (!space) {
                // Read new bytes if needed
                if (byteBuffer.position() >= byteBuffer.limit()) {
                    if (!fill(false)) // request line parsing
                        return false;
                }
                // Spec says method name is a token followed by a single SP but
                // also be tolerant of multiple SP and/or HT.
                int pos = byteBuffer.position();
                byte chr = byteBuffer.get();
                if (chr == Constants.SP || chr == Constants.HT) {
                    space = true;
                    //请求的方法(get/post)
                    request.method().setBytes(byteBuffer.array(), parsingRequestLineStart,
                            pos - parsingRequestLineStart);
                } else if (!HttpParser.isToken(chr)) {
                    byteBuffer.position(byteBuffer.position() - 1);
                    throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
                }
            }

看代码段,request.method().setBytes并没有把请求报文的请求方法转义为GET/POST字符,而是使用MessageBytes存储了请求报文(即inputBuffer.byteBuffer)起始位到第一个空格之前的字节数组的下标。 在使用的时候将字节转为GET/POST

第三步就是读取请求头inputBuffer.parseHeaders():过程类似读取请求行

第四步读取请求头后会执行prepareRequest():此方法设置request的filters和一些信息的设置。


第五步调用Adapter.service(request, response):将tomcat的内部coyoteRequest和coyoteReponse转换为servlet规范request ,response对象。这里有个一转换的过程。 就是创建servlet规范request ,response对象。然后将coyoteRequest,coyoteReponse分别设置给request,response 接下来就是调用各级容器,走过filter到达servlet中

// Calling the container
 connector.getService().getContainer().getPipeline().getFirst().invoke(
                        request, response);


8. HTTP协议body的解析


HTTP协议请求body的解析延迟到servlet中在获取参数的时候解析的。body的解析放到其他章节在讲。


总结下:


数据从连接通道copy到堆外内存,然后从堆外内存copy到 tomcat Http11InputBuffer的堆内byteBuffer。然后根据HTTP协议解析byteBuffer中的字节数组。变成HTTP协议的coyoteRequest,coyoteReponse。最后包装成我们常用的request,response对象。


重用:


Tomcat中有很多重用的组件.以减少频繁创建和销毁的开销

  • NioChannel:NioChannel channel = nioChannels.pop();
  • PollerEvent: PollerEvent r = eventCache.pop();
  • SocketProcessor:SocketProcessorBase sc = processorCache.pop();
  • Processor:Processor processor = connections.get(socket);

相关文章
|
设计模式 安全 Java
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
【分布式技术专题】「Tomcat技术专题」 探索Tomcat技术架构设计模式的奥秘(Server和Service组件原理分析)
262 0
|
7月前
|
消息中间件 监控 算法
用Apifox调试Socket.IO接口,从原理到实践
传统HTTP协议"请求-响应"的离散式通信机制已难以满足需求,这正是Socket.IO这类实时通信框架的价值所在。
用Apifox调试Socket.IO接口,从原理到实践
|
缓存 网络协议 Linux
c++实战篇(三) ——对socket通讯服务端与客户端的封装
c++实战篇(三) ——对socket通讯服务端与客户端的封装
397 0
|
12月前
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【10月更文挑战第10天】网络协议定义了机器间通信的标准格式,确保信息准确无损地传输。主要分为两种模型:OSI七层模型与TCP/IP模型。
|
12月前
|
前端开发 Java 应用服务中间件
21张图解析Tomcat运行原理与架构全貌
【10月更文挑战第2天】本文通过21张图详细解析了Tomcat的运行原理与架构。Tomcat作为Java Web开发中最流行的Web服务器之一,其架构设计精妙。文章首先介绍了Tomcat的基本组件:Connector(连接器)负责网络通信,Container(容器)处理业务逻辑。连接器内部包括EndPoint、Processor和Adapter等组件,分别处理通信、协议解析和请求封装。容器采用多级结构(Engine、Host、Context、Wrapper),并通过Mapper组件进行请求路由。文章还探讨了Tomcat的生命周期管理、启动与停止机制,并通过源码分析展示了请求处理流程。
|
12月前
|
网络协议 安全 Java
Java Socket原理
Java Socket原理是指在Java中通过Socket实现的网络通信的基础理论与机制。Socket是网络中不同设备间通信的一种标准方式,它允许应用程序之间通过TCP/IP等协议进行数据交换。在Java中,利用Socket编程可以方便地创建客户端与服务器端应用,实现跨网络的数据传输功能,是互联网软件开发中的重要技术之一。它支持多种通信模式,如可靠的流式套接字(TCP)和数据报式套接字(UDP)。
196 10
|
12月前
|
负载均衡 应用服务中间件 Apache
Tomcat负载均衡原理详解及配置Apache2.2.22+Tomcat7
Tomcat负载均衡原理详解及配置Apache2.2.22+Tomcat7
182 3
|
网络协议 Linux 应用服务中间件
Socket通信之网络协议基本原理
【9月更文挑战第14天】网络协议是机器间交流的约定格式,确保信息准确传达。主要模型有OSI七层与TCP/IP模型,通过分层简化复杂网络环境。IP地址全局定位设备,MAC地址则在本地网络中定位。网络分层后,数据包层层封装,经由不同层次协议处理,最终通过Socket系统调用在应用层解析和响应。
|
Java 数据安全/隐私保护
深入剖析:Java Socket编程原理及客户端-服务器通信机制
【6月更文挑战第21天】Java Socket编程用于构建网络通信,如在线聊天室。服务器通过`ServerSocket`监听,接收客户端`Socket`连接请求。客户端使用`Socket`连接服务器,双方通过`PrintWriter`和`BufferedReader`交换数据。案例展示了服务器如何处理每个新连接并广播消息,以及客户端如何发送和接收消息。此基础为理解更复杂的网络应用奠定了基础。
173 13
|
网络协议 网络安全 程序员
socket,tcp,http三者之间的原理和区别
socket,tcp,http三者之间的原理和区别
socket,tcp,http三者之间的原理和区别