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参数判断是否调用连接关闭的系统调用。
|