RST表示连接重置,用于关闭那些已经没有必要继续存在的连接。一般情况下表示异常关闭连接,区别与四次分手正常关闭连接。

产生RST的三个条件是:

  1. 目的地为某端口的SYN到达,然而在该端口上并没有正在监听的服务器;
  2. TCP想取消一个已有连接;
  3. TCP接收到一个根本不存在的连接上的分节。

下面的几种场景,都会产生RST,以此来说明重置报文段的用途。

一、针对不存在端口的连接请求

客户端向服务端某端口发起连接请求SYN,但是目的服务端主机不存在该端口,此时向客户端回应RST,中断连接请求。

下面通过程序和抓包进行分析。程序源码如下:

use std::io::prelude::*;
use std::net::TcpStream;
use std::thread; fn main() {
let mut stream = TcpStream::connect("192.168.2.229:33333").unwrap();
let n = stream.write(&[1,2,3,4,5,6,7,8,9]).unwrap();
println!("send {} bytes to remote node, waiting for end.", n); loop{
thread::sleep_ms(1000);
}
}

上面程序向目的主机192.168.2.229发起TCP连接,而目的主机并没有启动端口为33333的监听服务。所以当本地主机向目的主机发起TCP连接后,会收到来自目的主机的RST,并断开连接。(当然也不是所有的都会回复RST,有的主机可能不会进行回复)。抓包如下:

本地主机向目的主机发送TCP连接SYN

目的主机向本地主机回复ACK、RST

二、终止一条连接

终止一条连接的正常方法是由通信一方发送一个FIN。这种方法也被称为有序释放。因为FIN是在之前所有排队数据都已发送后才被发送出去,通常不会出现丢失数据的情况。然而在任何时刻,我们都可以通过发送一个重置报文段RST替代FIN来终止一条连接。这种方式也被称为终止释放。

终止一条连接可以为应用程序提供两大特性:

  • 任何排队的数据都将被抛弃,一个重置报文段会被立即发送出去;
  • 重置报文段的接收方会说明通信的另一端采用了终止的方式而不是一次正常关闭。API必须提供一种实现上述终止行为的方式来取代正 常的关闭操作。

套接字API可通过套接字选项SO_LINGER的数值设置为0来实现上述功能。

/* Structure used to manipulate the SO_LINGER option.  */
struct linger
{
int l_onoff; /* Nonzero to linger on close. */
int l_linger; /* Time to linger. */
};

SO_LINGER的不同值的含义如下:

  1. l_onoff0l_linger的值被忽略,内核缺省情况,close()调用会立即返回给调用者,TCP模块负责尝试发送残留的缓存区数据。
  2. l_onoff为非零值,l_linger0,则连接立即终止,TCP将丢弃残留在发送缓冲区中的数据并发送RST给对方,而不是发送FIN,这样避免了TIME_WAIT状态,对方read()时将收到Connection reset by peer的错误。
  3. l_onoff为非零值,l_linger大于零:如果l_linger时间范围,TCP模块成功发送完残留的缓冲区数据,则正常关闭,如果超时,则向对方发送RST,丢弃残留在发送缓冲区的数据。

客户端代码间附录代码1,服务端代码如下:

/* echo server with poll */
#include <poll.h>
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h> #define OPEN_MAX 1024
#define LISTEN_PORT 33333
#define MAX_BUF 1024 int set_linger(int sock, int l_onoff, int l_linger);
int handle_conn(struct pollfd *nfds, char* buf);
void run(); int main(int _argc, char* _argv[]) {
run(); return 0;
} void run() {
// bind socket
char str[INET_ADDRSTRLEN];
struct sockaddr_in seraddr, cliaddr;
socklen_t cliaddr_len = sizeof(cliaddr);
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
bzero(&seraddr, sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
seraddr.sin_port = htons(LISTEN_PORT); int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if (-1 == bind(listen_sock, (struct sockaddr*)&seraddr, sizeof(seraddr))) {
perror("bind server addr failure.");
exit(EXIT_FAILURE);
}
listen(listen_sock, 5); int ret, i;
struct pollfd nfds[OPEN_MAX];
for (i=0;i<OPEN_MAX;++i){
nfds[i].fd = -1;
} nfds[0].fd = listen_sock;
nfds[0].events = POLLIN; char* buf = (char*)malloc(MAX_BUF);
while (1) {
ret = poll(nfds, OPEN_MAX, NULL);
if (-1 == ret) {
perror("poll failure.");
exit(EXIT_FAILURE);
} /* An event on one of the fds has occurred. */
if (nfds[0].revents & POLLIN) {
int conn_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (-1 == conn_sock) {
perror("accept failure.");
exit(EXIT_FAILURE);
}
printf("accept from %s:%d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); set_linger(conn_sock, 1, 0); //设置SO_LINGER option值为0
for (int k=0;k<OPEN_MAX;++k){
if (nfds[k].fd < 0){
nfds[k].fd = conn_sock;
nfds[k].events = POLLIN;
break;
}
if (k == OPEN_MAX-1){
perror("too many clients, nfds size is not enough.");
exit(EXIT_FAILURE);
}
}
} handle_conn(nfds, buf);
} close(listen_sock);
} int handle_conn(struct pollfd *nfds, char* buf) {
int n = 0;
for (int i=1;i<OPEN_MAX;++i) {
if (nfds[i].fd<0) {
continue;
} if (nfds[i].revents & POLLIN) {
bzero(buf, MAX_BUF);
n = read(nfds[i].fd, buf, MAX_BUF);
if (0 == n) {
close(nfds[i].fd);
nfds[i].fd = -1;
continue;
}
if (n>0){
printf("recv from client: %s\n", buf);
nfds[i].events = POLLIN; close(nfds[i].fd); //接收数据后就主动关闭连接,用于RST测试
} else {
perror("read failure.");
exit(EXIT_FAILURE);
}
} else if (nfds[i].revents & POLLOUT) {
printf("write data to client: %s\n", buf);
write(nfds[i].fd, buf, sizeof(buf));
bzero(buf, MAX_BUF); nfds[i].events = POLLIN;
}
} return 0;
} int set_linger(int sock, int l_onoff, int l_linger) {
struct linger so_linger;
so_linger.l_onoff = l_onoff;
so_linger.l_linger = l_linger;
int r = setsockopt(sock, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger)); return r;
}

抓包结果如下:



先3次握手,后客户端向服务度发送了5个字节数据,服务端在接收完5字节数据向客户端ACK后,表示想中断连接,此时因设置了SO_LINGER选项值为0close()时,直接向对方发送RST而不是正常的发送FIN,连接立即终止,并且不会有TIME_WAIT状态,TCP将丢弃残留在发送缓冲区中的数据,对方read()时将收到Connection reset by peer的错误。

三、半开连接

如果在未告知另一端的情况下通信的一端关闭或终止连接,那么就认为该条TCP连接处于半开状态。

举个例子,服务器主机被切断电源后重启(切断电源前可将网线断开,重启后再接上),此时的客户端是一个半开的连接。当客户端再次向服务端发送数据时,服务端对此连接一无所知,会回复一个重置报文段RST后,中断连接。

再或者如果程序开启了TCP保活机制,则当监测到对方主机不可达时,发送RST中断连接。详细可参考我的另一篇博文TCP保活机制

TCP连接如果长时间没有数据收发,会使TCP发送保活探测报文,以维持连接或者探测连接是否存在。



可以看到如果认为连接不存在了,就会发送RST中断连接。

四、提前关闭连接

TCP应用程序接收数据是从操作系统中接收的TCP数据,如果数据到达了操作系统但是我应用数据不想继续接收数据了,此时RST中断连接。

服务端代码:

/* echo server with poll */
#include <poll.h>
#include<stdio.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<errno.h>
#include<pthread.h> #define OPEN_MAX 1024
#define LISTEN_PORT 33333
#define MAX_BUF 1024 #define RST_TEST 1 int handle_conn(struct pollfd *nfds, char* buf);
void run(); int main(int _argc, char* _argv[]) {
run(); return 0;
} void run() {
// bind socket
char str[INET_ADDRSTRLEN];
struct sockaddr_in seraddr, cliaddr;
socklen_t cliaddr_len = sizeof(cliaddr);
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
bzero(&seraddr, sizeof(seraddr));
seraddr.sin_family = AF_INET;
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
seraddr.sin_port = htons(LISTEN_PORT); int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
if (-1 == bind(listen_sock, (struct sockaddr*)&seraddr, sizeof(seraddr))) {
perror("bind server addr failure.");
exit(EXIT_FAILURE);
}
listen(listen_sock, 5); int ret, i;
struct pollfd nfds[OPEN_MAX];
for (i=0;i<OPEN_MAX;++i){
nfds[i].fd = -1;
} nfds[0].fd = listen_sock;
nfds[0].events = POLLIN; char* buf = (char*)malloc(MAX_BUF);
while (1) {
ret = poll(nfds, OPEN_MAX, NULL);
if (-1 == ret) {
perror("poll failure.");
exit(EXIT_FAILURE);
} /* An event on one of the fds has occurred. */
if (nfds[0].revents & POLLIN) {
int conn_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (-1 == conn_sock) {
perror("accept failure.");
exit(EXIT_FAILURE);
}
printf("accept from %s:%d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (int k=0;k<OPEN_MAX;++k){
if (nfds[k].fd < 0){
nfds[k].fd = conn_sock;
nfds[k].events = POLLIN;
break;
}
if (k == OPEN_MAX-1){
perror("too many clients, nfds size is not enough.");
exit(EXIT_FAILURE);
}
}
} handle_conn(nfds, buf);
} close(listen_sock);
} int handle_conn(struct pollfd *nfds, char* buf) {
int n = 0;
for (int i=1;i<OPEN_MAX;++i) {
if (nfds[i].fd<0) {
continue;
} if (nfds[i].revents & POLLIN) {
bzero(buf, MAX_BUF);
#if RST_TEST == 0
n = read(nfds[i].fd, buf, MAX_BUF);
#else
n = read(nfds[i].fd, buf, 5); //只接收部分数据就主动关闭连接,用于RST测试
#endif
if (0 == n) {
close(nfds[i].fd);
nfds[i].fd = -1;
continue;
}
if (n>0){
printf("recv from client: %s\n", buf);
nfds[i].events = POLLOUT;
#if RST_TEST != 0
close(nfds[i].fd); //只接收部分数据就主动关闭连接,用于RST测试
#endif
} else {
perror("read failure.");
exit(EXIT_FAILURE);
}
} else if (nfds[i].revents & POLLOUT) {
printf("write data to client: %s\n", buf);
write(nfds[i].fd, buf, sizeof(buf));
bzero(buf, MAX_BUF); nfds[i].events = POLLIN;
}
} return 0;
}

客户端发起连接后发送超过5字节的数据后,因为服务端只接收5个字节数据后不再接收数据(此时服务端操作系统已经收到10个字节数据,但上层read系统调用只接收5个字节),服务端向客户端发送RST中断连接。抓包结果如下:



先3次握手,握手后客户端发送了10字节长度的数据,服务端在回应客户端ACK接收到数据后,发送RST中断连接。

五、在一个已关闭的TCP连接上收到数据

如果一个已关闭的TCP连接又收到数据,显然是异常的,此时应RST中断连接。

服务端其他代码与上个代码相同,下面函数替换一下

int handle_conn(struct pollfd *nfds, char* buf) {
int n = 0;
for (int i=1;i<OPEN_MAX;++i) {
if (nfds[i].fd<0) {
continue;
} if (nfds[i].revents & POLLIN) {
bzero(buf, MAX_BUF);
n = read(nfds[i].fd, buf, MAX_BUF);
if (0 == n) {
close(nfds[i].fd);
nfds[i].fd = -1;
continue;
}
if (n>0){
printf("recv from client: %s\n", buf);
nfds[i].events = POLLOUT; close(nfds[i].fd); //接收数据后就主动关闭连接,用于RST测试
} else {
perror("read failure.");
exit(EXIT_FAILURE);
}
} else if (nfds[i].revents & POLLOUT) {
printf("write data to client: %s\n", buf);
write(nfds[i].fd, buf, sizeof(buf));
bzero(buf, MAX_BUF); nfds[i].events = POLLIN;
}
} return 0;
}

客户端代码与上个相同,只有下面函数不同,替换一下即可:

void client_handle(int sock) {
char sendbuf[MAXLEN], recvbuf[MAXLEN];
bzero(sendbuf, MAXLEN);
bzero(recvbuf, MAXLEN);
int n = 0; while (1) {
if (NULL == fgets(sendbuf, MAXLEN, stdin)) {
break;
}
// 按`#`号退出
if ('#' == sendbuf[0]) {
break;
}
struct timeval start, end;
gettimeofday(&start, NULL);
write(sock, sendbuf, strlen(sendbuf));
sleep(2);
write(sock, sendbuf, strlen(sendbuf)); //这里是测试RST用的代码
sleep(60);
n = read(sock, recvbuf, MAXLEN);
if (0 == n) {
break;
}
write(STDOUT_FILENO, recvbuf, n);
gettimeofday(&end, NULL);
printf("time diff=%ld microseconds\n", ((end.tv_sec * 1000000 + end.tv_usec)- (start.tv_sec * 1000000 + start.tv_usec)));
} close(sock);
}

抓包如下:



先3次握手;后客户端向服务端发送了5字节数据,服务端接收到5字节数据回复ACK;之后向客户端发送FIN,关闭连接,但此时客户端还有数据要发送,没有向服务端发起FIN,此时只进行了2次挥手;之后客户端又向服务端发送了5个字节数据,但此时服务端该连接已经调用close()关闭,此时再次收到该连接的数据属于异常,回复RST中断连接。

六、附录

测试用的客户端代码

#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include <time.h>
#include <sys/time.h>
#include<stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <string.h> #define SERVER_PORT 33333
#define MAXLEN 65535 void client_handle(int sock); int main(int argc, char* argv[]) {
for (int i = 1; i < argc; ++i) {
printf("input args %d: %s\n", i, argv[i]);
}
struct sockaddr_in seraddr;
int server_port = SERVER_PORT;
if (2 == argc) {
server_port = atoi(argv[1]);
} int sock = socket(AF_INET, SOCK_STREAM, 0);
bzero(&seraddr, sizeof(seraddr));
seraddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr);
seraddr.sin_port = htons(server_port); if (-1 == connect(sock, (struct sockaddr *)&seraddr, sizeof(seraddr))) {
perror("connect failure");
exit(EXIT_FAILURE);
}
client_handle(sock); return 0;
} void client_handle(int sock) {
char sendbuf[MAXLEN], recvbuf[MAXLEN];
bzero(sendbuf, MAXLEN);
bzero(recvbuf, MAXLEN);
int n = 0; while (1) {
if (NULL == fgets(sendbuf, MAXLEN, stdin)) {
break;
}
// 按`#`号退出
if ('#' == sendbuf[0]) {
break;
}
struct timeval start, end;
gettimeofday(&start, NULL);
write(sock, sendbuf, strlen(sendbuf));
n = read(sock, recvbuf, MAXLEN);
if (n < 0) {
perror("read failure.");
exit(EXIT_FAILURE);
}
if (0 == n) {
break;
}
write(STDOUT_FILENO, recvbuf, n);
gettimeofday(&end, NULL);
printf("time diff=%ld microseconds\n", ((end.tv_sec * 1000000 + end.tv_usec)- (start.tv_sec * 1000000 + start.tv_usec)));
} close(sock);
}

欢迎关注微信公众号,推送计算机网络、后端开发、区块链、分布式、Rust、Linux等技术文章!

TCP重置报文段及RST常见场景分析的更多相关文章

  1. JVM之调优及常见场景分析

    JVM调优 GC调优是最后要做的工作,GC调优的目的可以总结为下面两点: 减少对象晋升到老年代的数量 减少FullGC的执行时间 通过监控排查问题及验证优化结果,可以分为: 命令监控:jps.jinf ...

  2. TCP最大报文段MSS源码分析

    概述 本文主要对MSS相关的几个字段结合源码流程进行分析: 字段含义 user_mss(tcp_options_received)–用户配置的mss,优先级最高: mss_clamp(tcp_opti ...

  3. TCP 重置攻击的工作原理

    原文链接:https://fuckcloudnative.io/posts/deploy-k3s-cross-public-cloud/ TCP 重置攻击 是使用一个单一的数据包来执行的,只有几个字节 ...

  4. TCP之RST报文段

    TCP 首部中的 RST 比特是用于 "复位" 的.一般来说,无论何时一个报文段发往基准的连接(referenced connection)出现错误,TCP 都会发出一个复位报文段 ...

  5. TCP的报文详细解读

    这张图好像挺有名的,其实一开始我看见的时候是一脸懵逼的,但是通过翻书(大学时代最害怕的计算机网络),查阅他人博客等等办法,最后终于有了一个系统的了解,当然,这里知识点多而杂,大家可以多看几遍,结合上面 ...

  6. TCP报文格式和三次握手——三次握手三个tcp包(header+data),此外,TCP 报文段中的数据部分是可选的,在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP 首部。

    from:https://blog.csdn.net/mary19920410/article/details/58030147 TCP报文是TCP层传输的数据单元,也叫报文段. 1.端口号:用来标识 ...

  7. TCP/IP详解--发送ACK和RST的场景

    在有以下几种情景,TCP会把ack包发出去: 1.收到1个包,启动200ms定时器,等到200ms的定时器到点了(第二个包没来),于是对这个包的确认ack被发送.这叫做“延迟发送”: 2.收到1个包, ...

  8. 计算机网络(三),TCP报文段详解

    目录 1.TCP(Transmission Control Protocol传输控制协议)作用 2.TCP报文段详解 三.TCP报文段详解 1.TCP(Transmission Control Pro ...

  9. 计算机网络(8)-----TCP报文段的首部格式

    TCP报文段的首部格式 概述 TCP报文段首部的前20个字节是固定的,因此TCP首部的最小长度是20字节. 源端口和目标端口 各占2个字节,分别写入源端口号和目的端口号. 序列号 占4个字节,表示本报 ...

随机推荐

  1. Azkaban —— 编译及部署

    一.Azkaban 源码编译 1.1 下载并解压 Azkaban 在3.0版本之后就不提供对应的安装包,需要自己下载源码进行编译. 下载所需版本的源码,Azkaban的源码托管在GitHub上,地址为 ...

  2. 一篇文章概括 Java Date Time 的使用

    本文目的:掌握 Java 中日期和时间常用 API 的使用. 参考:Jakob Jenkov的英文教程Java Date Time Tutorial 和 JavaDoc 概览 Java 8 新增 AP ...

  3. 面试中的作用域题和THIS 指向的问题

    作用域的面试题 1. fn() function fn () { console.log(12) } var as = function () { console.log(45) } 2. var a ...

  4. PATB 1038. 统计同成绩学生(20)

    https://www.patest.cn/contests/pat-b-practise/1038 #include <cstdio> int cnt[110]; int temp[10 ...

  5. 02_javaSE面试题:单例设计模式

    还记得很多年前,面试就让在白板上写个单例模式,当时技术渣渣,还写的是class A.面试官还说,你就不能写个Singleton. 面试题 编程题:写一个Singleton示例 解析 什么是Single ...

  6. POJ 2955:Brackets(区间DP)

    http://poj.org/problem?id=2955 题意:给出一串字符,求括号匹配的数最多是多少. 思路:区间DP. 对于每个枚举的区间边界,如果两边可以配对成括号,那么dp[i][j] = ...

  7. 使用PowerShell比较本地文本文件与Web上的文本文件是否相同

    使用PowerShell比较本地文本文件是否相同通常有两种方式:1.通过Get-FileHash这个命令,比较两个文件的哈希是否相同:2.通过Compare-Object这个命令,逐行比较两个文件的内 ...

  8. Logstash : 从 SQL Server 读取数据

    有些既存的项目把一部分日志信息写入到数据库中了,或者是由于其它的原因我们希望把关系型数据库中的信息读取到 elasticsearch 中.这种情况可以使用 logstash 的 jdbc input ...

  9. Bzoj1972: [Sdoi2010]猪国杀 题解(大模拟+耐心+细心)

    猪国杀 - 可读版本 https://mubu.com/doc/2707815814591da4 题目可真长,读题都要一个小时. 这道题很多人都说不可做,耗时间,代码量大,于是,本着不做死就不会死的精 ...

  10. python函数知识二 动态参数、函数的注释、名称空间、函数的嵌套、global,nonlocal

    6.函数的动态参数 *args,**kwargs:能接受动态的位置参数和动态的关键字参数 *args -- tuple *kwargs -- dict 动态参数优先级:位置参数 > 动态位置参数 ...