HTTP长连接解析

1.先回顾一下网络四层模型

2.TCP的三次握手和四次挥手

这是三次握手和四次挥手的主要流程,其中我们可以看到 对于已经建立的TCP连接,一定是有一方主动进行关闭发送Fin连接才会关闭,那我们就探究一下是不是这样的

3.使用一个java的socket进行测试

这个测试是在tcp层玩,没有引入http层概念

//服务器
public class server {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        Socket socket = null;
        ByteArrayOutputStream baos = null;
        InputStream is = null;
        try {
            //1.我要有一个地址
            serverSocket = new ServerSocket(9999);
            //2.等待客户端连接过来  监听
            //Socket accept() 监听要对这个套接字作出的连接并接受它 
            socket = serverSocket.accept();
        
            // 在这里进行睡眠,保证程序不退出,连接一直不主动关闭
            Thread.sleep(100000);
​
            //3.读取客户端的消息
            is = socket.getInputStream();
            //管道流
            baos = new ByteArrayOutputStream();
            byte[] buffer =new byte[1024];
            int len;
            while ((len=is.read(buffer))!=-1){
                baos.write(buffer,0,len);
            }
            System.out.println(baos.toString());
​
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            //关闭资源
            if (baos!=null){
                try {
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (is!=null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket!=null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket!=null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
//客户端
public class client {
    public static void main(String[] args) {
        Socket socket = null;
        OutputStream os = null;
        try {
            //1.要知道服务器的地址,端口号
            //想要去连接的ip和端口
            InetAddress serviceIP = InetAddress.getByName("192.168.76.92");
            int port = 9999;
            //2.创建一个ServerSocket连接   做了好多件事
            socket = new Socket(serviceIP,port);
            
            // 在这里进行睡眠,保证程序不退出,连接一直不主动关闭
            Thread.sleep(1000000);
            //3.发送消息  IO流
            //要通过 socket 向绑定 9999 端口的客服端传出去数据,所以用 socket.getOutputStream()。
            //socket.getOutputStream()得到的就是流对象,只需赋值给os就可以
            os = socket.getOutputStream();
            os.write("你好Java".getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (os!=null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket!=null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
通过Linux的netstat -natp可以发现如下结果:

 

因此得出结论,在使用 网络进行通信的时候,TCP层并不会自动帮我们关闭连接,而是维护  ESTABLISHED状态

4.修改client代码,主动关闭再次测试

//客户端
public class client {
    public static void main(String[] args) {
        Socket socket = null;
        OutputStream os = null;
        try {
            //1.要知道服务器的地址,端口号
            //想要去连接的ip和端口
            InetAddress serviceIP = InetAddress.getByName("192.168.76.92");
            int port = 9999;
            //2.创建一个ServerSocket连接   做了好多件事
            socket = new Socket(serviceIP,port);
            
            Thread.sleep(5000);
            // client 端主动进行关闭 
            socket.close();
            // 在这里进行睡眠,保证程序不退出,连接一直不主动关闭 
            Thread.sleep(1000000);
            //3.发送消息  IO流
            //要通过 socket 向绑定 9999 端口的客服端传出去数据,所以用 socket.getOutputStream()。
            //socket.getOutputStream()得到的就是流对象,只需赋值给os就可以
            Thread.sleep(1000000);
            os = socket.getOutputStream();
            os.write("你好Java".getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (os!=null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (socket!=null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
在这种情况下,我们server端还是保证睡眠,一直不关闭,而client主动进行关闭了。
通过Linux的netstat -natp分别观察两边的情况:

 

由此可见,socket.close();才是进行连接关闭的主要行为,并且双发都要主动调用socket.close();否则资源还是开启的

5.经过一段时间再查看

 

 

 

发现 client端在维持一段 FIN_WAIT 2 状态后资源主动进行关闭 FIN_WAIT 2消失,而server的CLOSE_WAIT状态将一直维持不会自动消失,这就表明如果Server是一个web服务,不主动关闭那么资源会一直占用,导致服务器承受巨大的压力。

6.推理

因此,对于HTTP的长短连接行为 在于HTTP协议的使用者(应用层)是否主动调用tcp层的关闭行为。
​
对于Tomcat服务器,在它的源码中会对http请求进行解析
1. 解析http版本
    1.0版本不支持,收到请求处理完,数据写回直接调用close关闭
    1.1版本,由Connection请求参数控制
2. 解析Connection参数
    如果false,和1.0一样,收到请求处理完,数据写回直接调用close关闭
    如果keepAlive,通过线程监控keepAliveTimeout,到时间调用close关闭

7.查看Tomcat源码

1.创建协议处理对象

//AbstractHttp11Protocol类中
    @Override
    protected Processor createProcessor() {
        // 创建对象
        Http11Processor processor = new Http11Processor(this, getEndpoint());
        processor.setAdapter(getAdapter());
        processor.setMaxKeepAliveRequests(getMaxKeepAliveRequests());
        processor.setConnectionUploadTimeout(getConnectionUploadTimeout());
        processor.setDisableUploadTimeout(getDisableUploadTimeout());
        processor.setRestrictedUserAgents(getRestrictedUserAgents());
        processor.setMaxSavePostSize(getMaxSavePostSize());
        return processor;
    }

2.通过处理类进行解析

//Http11Processor类中
public SocketState service(SocketWrapperBase<?> socketWrapper) throws IOException {
    //这里代码太多了挑点核心的
        keepAlive = true;
        comet = false;
        openSocket = false;
        sendfileInProgress = false;
        readComplete = true;
        // NioEndpoint返回true, Bio返回false
        if (endpoint.getUsePolling()) {
            keptAlive = false;
        } else {
            keptAlive = socketWrapper.isKeptAlive();
        }
​
        // 如果当前活跃的线程数占线程池最大线程数的比例大于75%,那么则关闭KeepAlive,不再支持长连接
        if (disableKeepAlive()) {
            socketWrapper.setKeepAliveLeft(0);
        }
​
        // keepAlive默认为true,它的值会从请求Request Headers中读取
        // 因此这是个Loop循环方法,表示持续执行下面的重复或者数据过程
        // 直到这个表达式中结果为false,不再进行
        while (!getErrorState().isError() && keepAlive && !comet && !isAsync() &&
                upgradeInbound == null &&
                httpUpgradeHandler == null && !endpoint.isPaused()) {
            // keepAlive如果为true,接下来需要从socket中不停的获取http请求
​
            // Parsing the request header
            try {
                // 第一次从socket中读取数据,并设置socket的读取数据的超时时间
                // 对于BIO,一个socket连接建立好后,不一定马上就被Tomcat处理了,其中需要线程池的调度,所以这段等待的时间要算在socket读取数据的时间内
                // 而对于NIO而言,没有阻塞
                setRequestLineReadTimeout();
​
                // 解析请求行
                if (!getInputBuffer().parseRequestLine(keptAlive)) {
                    // 下面这个方法在NIO时有用,比如在解析请求行时,如果没有从操作系统读到数据,则上面的方法会返回false
                    // 而下面这个方法会返回true,从而退出while,表示此处read事件处理结束
                    // 到下一次read事件发生了,就会从小进入到while中
                    if (handleIncompleteRequestLineRead()) {
                        break;
                    }
                }
...
 if (maxKeepAliveRequests == 1) {
                // 如果最大的活跃http请求数量仅仅只能为1的话,那么设置keepAlive为false,则不会继续从socket中获取Http请求了
                keepAlive = false;
            } else if (maxKeepAliveRequests > 0 &&
                    socketWrapper.decrementKeepAlive() <= 0) {
                // 如果已经达到了keepAlive的最大限制,也设置为false,则不会继续从socket中获取Http请求了
                keepAlive = false;
        }
        // 到这里是while(XXX) 循环结束了,不管是怎么结束的(判断好复杂我也没细看),反正是数据处理完了,要关闭连接也就是tcp层close了     
                
        if (getErrorState().isError() || (endpoint.isPaused() && !isAsync())) {
            return SocketState.CLOSED;
        } else if (isAsync()) {
            return SocketState.LONG;
        } else if (isUpgrade()) {
            return SocketState.UPGRADING;
        } else {
            if (sendfileState == SendfileState.PENDING) {
                return SocketState.SENDFILE;
            } else {
                if (openSocket) {
                    if (readComplete) {
                        return SocketState.OPEN;
                    } else {
                        return SocketState.LONG;
                    }
                } else {
                    return SocketState.CLOSED;
                }
            }
        }
       // 反正一堆close,但是这个close没干活,他是一个枚举类相当于一个标志位!
}

3.随便找一个对枚举标志位的处理

这里随便找到,大体的关闭逻辑应该是差不多的
//Nio2Endpoint类中
​
if (state == SocketState.CLOSED) {
// Close socket and pool
socketWrapper.close();
​
// SocketWrapperBase
    public void close() {
        if (closed.compareAndSet(false, true)) {
            try {
                getEndpoint().getHandler().release(this);
            } catch (Throwable e) {
                ExceptionUtils.handleThrowable(e);
                if (log.isDebugEnabled()) {
                    log.error(sm.getString("endpoint.debug.handlerRelease"), e);
                }
            } finally {
                getEndpoint().countDownConnection();
                doClose();
            }
        }
    }
    
    
// Nio2Endpoint类的     我把影响体验的删一删
        @Override
        protected void doClose() {
            try {
                getEndpoint().connections.remove(getSocket().getIOChannel());
                // 获取到socket     调用socket.close
                // 这不就和  标题4.中修改client代码,主动关闭再次测试 一样的道理吗
                if (getSocket().isOpen()) {
                    getSocket().close(true);
                }
                if (getEndpoint().running) {
                    if (nioChannels == null || !nioChannels.push(getSocket())) {
                        getSocket().free();
                    }
                }
            } catch (Throwable e) {
                ExceptionUtils.handleThrowable(e);
                if (log.isDebugEnabled()) {
                    log.error(sm.getString("endpoint.debug.channelCloseFail"), e);
                }
            } finally {
                socketBufferHandler = SocketBufferHandler.EMPTY;
                nonBlockingWriteBuffer.clear();
                reset(Nio2Channel.CLOSED_NIO2_CHANNEL);
            }
        }   

8.总结

java中的socket是通过调用的操作系统(Linux,Windows)的socket接口进行通信的,socket帮我们完成了 底层通信,同时给我们提供了连接关闭的系统调用,我们在http层可以通过判断http协议传递过来的数据解析,根据keepalive参数判断是否调用连接关闭的系统调用。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
如果您下载了本程序,但是该程序存在问题无法运行,那么您可以选择退款或者寻求我们的帮助(如果找我们帮助的话,是需要追加额外费用的)。另外,您不会使用资源的话(这种情况不支持退款),也可以找我们帮助(需要追加额外费用) 爬虫(Web Crawler)是一种自动化程序,用于从互联网上收集信息。其主要功能是访问网页、提取数据并存储,以便后续分析或展。爬虫通常由搜索引擎、数据挖掘工具、监测系统等应用于网络数据抓取的场景。 爬虫的工作流程包括以下几个关键步骤: URL收集: 爬虫从一个或多个初始URL开始,递归或迭代地发现新的URL,构建一个URL队列。这些URL可以通过链接分析、站点地图、搜索引擎等方式获取。 请求网页: 爬虫使用HTTP或其他协议向目标URL发起请求,获取网页的HTML内容。这通常通过HTTP请求库实现,如Python中的Requests库。 解析内容: 爬虫对获取的HTML进行解析,提取有用的信息。常用的解析工具有正则表达式、XPath、Beautiful Soup等。这些工具帮助爬虫定位和提取目标数据,如文本、图片、链接等。 数据存储: 爬虫将提取的数据存储到数据库、文件或其他存储介质中,以备后续分析或展。常用的存储形式包括关系型数据库、NoSQL数据库、JSON文件等。 遵守规则: 为避免对网站造成过大负担或触发反爬虫机制,爬虫需要遵守网站的robots.txt协议,限制访问频率和深度,并模拟人类访问行为,如设置User-Agent。 反爬虫应对: 由于爬虫的存在,一些网站采取了反爬虫措施,如验证码、IP封锁等。爬虫工程师需要设计相应的策略来应对这些挑战。 爬虫在各个领域都有广泛的应用,包括搜索引擎索引、数据挖掘、价格监测、新闻聚合等。然而,使用爬虫需要遵守法律和伦理规范,尊重网站的使用政策,并确保对被访问网站的服务器负责。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值