从这张图可以看出IP报文由首部+数据组成,而IP数据又是TCP首部+数据的组成,首部就是对数据的描述,可以称之为元数据。
实际上发送方是一层层封包,也即在每一层加上自己的元数据。而接收方是一层层解包,就是在每一层根据元数据将其拆分,在运输层将其合并,在等待缓冲区填满时一并交给应用层
下面有6个控制位,说明报文段的性质
URG:当URG=1时,表明紧急指针字段有效。告诉系统此报文段有紧急数据,应该尽快传输,具有高优先级,不是按照队列顺序传输。比如给远程的主机发送很长的数据,等待很长的时间,这时可以通过Control+C中断传输,而这个命令就是紧急数据,排在要传输的数据的前面
ACK:只有大写的ACK=1时,小写的ack才生效。也即ACK=0时,ack无效。当然TCP规定,在连接建立后必须将ACK置为1
PSH:发送方将PSH置为1,立即创建报文段将其发送出去。接收方收到PSH=1的报文段时,会尽快将接收缓冲区的数据推给应用程序,不需要等到缓冲区填满再交给上层
RST:当RST=1时,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立连接。RST置为1还用来拒绝一个非法的报文段或拒绝打开一个连接
SYN:在连接建立的过程中使用,表示想与对方建立连接。当SYN=1,ACK=0时,表明是一个请求连接的报文。当SYN=1,ACK=1时,表明这是响应报文段,同意与之建立连接。
FIN:用来释放一个连接。当FIN=1时,表明数据已经发送完毕,要求关闭连接。
窗口:占用2字节。这是接收方的窗口,是接收方当前可以接收的字节数,而发送方根据此窗口控制发送数据的速度。这个窗口是动态变化的,最终也是接收方缓存的剩余空间决定的。
以上是基础知识,从本文的知识点来说,RST是重中之重。
在客户端请求与服务端连接时,要经过三次握手
问题1:seq是否不必要?重要的是SYN和ACK的标识符?肯定不是
问题2:如果只有两次握手可以吗?不可以,会出现以下问题:
当客户端已经传输完数据后,会主动关闭连接,然后经历四次挥手的过程
问题1:为何存在TIME_WAIT状态?
问题2:为何要挥手四次?
winshark是在Windows中的抓包工具,开启捕获操作时,通过过滤可以得到我们想要的抓包数据
tcp.port==8080,表明抓包TCP连接中端口号为8080的报文
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerDemo {
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
for(;;) {
Thread.sleep(1000);
socketChannel.read(byteBuffer);
socketChannel.write(ByteBuffer.wrap("hello client".getBytes()));
System.out.println("read success");
}
}
}
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class ClientDemo {
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(8080));
for (int i = 0; i < 2; i++) {
Thread.sleep(1000);
socketChannel.write(ByteBuffer.wrap("hello server".getBytes()));
System.out.println("write success");
}
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socketChannel.read(byteBuffer);
socketChannel.close();
Thread.sleep(10000);
}
}
通过winshark抓包:有个小疑问:为何所有write方法都会将PSH置1,Linux也是如此。
56590端口是客户端的,8080是服务端的。在这里只需关注控制位(FIN、ACK、RST)
为啥客户端会发送RST报文?我们猜一下是由于服务器还有数据发送导致客户端发送RST强制中断连接。那么简化一下代码,看看是否有RST报文出现
下面的代码服务端只是读取一次数据,而客户端只发送一次数据,双方都没有异常抛出。
客户端调用close:由于客户端要经历四次挥手,但服务端进程已经结束,第三次挥手无法响应,内核会发送RST报文给客户端,强制关闭连接
客户端未调用close:客户端结束进程,给服务端发送RST报文表明强制关闭连接
注意:如果一方已经发送RST报文,而另一方还是调用read/write操作,会有异常抛出。否则就没有异常
未调用close的情况下,是不是也有可能是服务端给客户端发送RST报文。给客户端加个睡眠,看看结果如何
由服务端发送RST报文给客户端。
以上代码服务端均未调用close关闭与客户端的连接,如果调用close会不会有RST报文?
报文显示有完整地四次挥手动作
小结:从当前的分析来看,如果没有正常调用close方法(服务端和客户端将socketChannel关闭),就结束进程,先结束的一方会发送RST报文给另一方,强制关闭连接
1、异常堆栈中显示最近一次调用是FileDispatcherImpl.read0,这是JNI方法,就从FileDispatcherImpl.c找到read0方法。
read(fd, buf, len)是系统调用,没有任何异常信息输出。可能性在于convertReturnVal函数中。
2、在IOUtil.c中找到convertReturnVal函数。前面几个判断都是特殊情况,而像我们这种异常会在最后一个else中,找一下JNU_ThrowIOExceptionWithLastError函数
3、获取最近一次错误信息,如果有就转换成java的String类型,调用Throw抛出异常
jin_util.c
4、errno是C函数库的全局变量,存储最近一次的错误号。strerror是C函数库中的全局函数,根据错误号获取对应的错误信息
jvm.cpp
os_linux.cpp
5、涉及到C函数库,我们从glibc中寻找。当接收到RST报文时,C函数库会转换成ECONNRESET错误信息
6、全局变量errno存放最近一次的错误号,如果多线程情况下,errno是不是会被覆盖。
extern:全局标识符
__thread:声明为线程局部变量
attribute_tls_model_ie:指定TLS模型
所以errno是TLS变量,并不会被其他线程覆盖
include/errno.h
小结:异常关闭连接时会由先结束的一方发送RST报文,如果另一方还继续操作,jvm会从glibc获取最近的错误号,从而得到异常信息,进而抛出Connection reset by peer