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