Lab4 TCPConnection

心得

耗时情况

【长舒一口气】

最开始先记录下总体耗时情况吧。本次实验共耗费我16h+【不包括接下来写笔记的时间23333】,共耗费三个工作日。第一天看完了指导书,写完了代码,过掉了#45 reorder之前的所有测试。第二天过掉了#55 t_ucS_1M_32k之前的所有测试,直到第三天才过完了所有测试。

我觉得这整个过程还是挺有意义的,每天都有新的进展,看到test case越过越多是真的很高兴。但是可以说第二天以来就都是面向测试用例改bug了,非常折磨非常坐牢,既要去再次理清之前写过的shit山,又得搞清楚很多让人一头雾水不知从何下手的地方。但总之,这三天很充实,并不会让人觉得心累。

放个通关截图吧,感人至深。

image-20230305160608116

思路

TCPConnection中具体要做什么,指导书已经写得很详细了,跟着指导书就行。代码部分还是不折磨的,思路直观清晰。

指导书内容

Here are the basic rules the TCPConnection has to follow:

  1. Receiving segments

    大概是在segment_received中做

    image-20230303103640065

    1. 检查RST flag

      如果RST被设置,sets both the inbound and outbound streams to the error state,杀死当前connection

      return;

      具体实现中,杀死connection可以置_linger_after_streams_finish为false。 the inbound and outbound streams对应着receiver和sender里的stream。让它们都处于error状态,只需设置ByteStream中的error字段

    2. 如果收到的segment with an invalid sequence number,connection需要发送empty segment应答

      image-20230303110238265

    3. 转发segment给receiver

    4. 如果ACK,则把ackno和win_size给sender

  2. Sending segments

    1. 任何时候sender把segment放进其out流,你都要从中取出来
    2. 从receiver处获取ackno和window_size,填入segment中
    3. 放到自己的segment_out中

    从上述表述中,我们需要注意两点:

    1. 顺带ACK

      可以看到,这跟我们上课的时候所学的一样,是“顺带ACK”,也即ACK报文并非独立发送,而是在下一次要发送其他数据报文的时候携带发送。这也一定程度上使得ack报文发送不会太频繁也不会太稀疏。

    2. 一定要经由sender

      我们如果想要发送一个报文,一定得先把它存入sender中,再从sender的segment_out中取出来。这样做的目的是把该报文列入sender的超时重传管辖范围,你如果直接把报文发送到自己的segment_out中,就无法管理其超时重传了

  3. When time passes

    tick()

    1. 调用sender的tick()
    2. 检查sender的连续超时重传次数,如果大于MAX RETX ATTEMPTS,则关闭连接,并且发送RST标志的空报文
    3. end the connection cleanly if necessary

再注意一点对于connection的关闭。它要求有一个time pass

image-20230303111131650

image-20230303112817891

第一点挺好实现的,第二点需要在析构函数中检测。

最后的5.1部分值得一看。

接口说明

TCPConnection的public函数接口定义以及具体要做什么如下。结合上面的指导书内容,TCPConnection的实现就很简单了,我就不多bb了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
 private:
TCPConfig _cfg;
// 一个endpoint可以同时作为sender和receiver。
TCPReceiver _receiver{_cfg.recv_capacity};
TCPSender _sender{_cfg.send_capacity, _cfg.rt_timeout, _cfg.fixed_isn};

//! outbound queue of segments that the TCPConnection wants sent
// 把要发送的segment放在这里就行了
std::queue<TCPSegment> _segments_out{};

//! Should the TCPConnection stay active (and keep ACKing)
//! for 10 * _cfg.rt_timeout milliseconds after both streams have ended,
//! in case the remote TCPConnection doesn't know we've received its whole stream?
bool _linger_after_streams_finish{true};

public:
// 也许需要调用TCPSender的fill_window(),然后从其segment_out中取出来,再发送给自己的segment_out
// Initiate a connection by sending a SYN segment初始化connection并且发送SYN
void connect();

/* 这几个都很好实现,都很直观,只需调sender和receiver的API就行 */
// 由上层socket调用,data路径 socket->connection->sender.stream_in().write()
//! \brief Write data to the outbound byte stream, and send it over TCP if possible
//! \returns the number of bytes from `data` that were actually written.
size_t write(const std::string &data);
//! \returns the number of `bytes` that can be written right now.
size_t remaining_outbound_capacity() const;
//! \brief Shut down the outbound byte stream (still allows reading incoming data)
void end_input_stream();
//! \brief The inbound byte stream received from the peer
ByteStream &inbound_stream() { return _receiver.stream_out(); }
// number of bytes sent and not yet acknowledged, counting SYN/FIN each as one byte
size_t bytes_in_flight() const;
//! \brief number of bytes not yet reassembled
size_t unassembled_bytes() const;

//! \brief Number of milliseconds since the last segment was received
size_t time_since_last_segment_received() const;

// debug用
//!< \brief summarize the state of the sender, receiver, and the connection
TCPState state() const { return {_sender, _receiver, active(), _linger_after_streams_finish}; };

// 这些函数都会由上层在某些时候调用
// 时钟滴答、收到segment以及从segment_out中取数据,这些都是由os调用相应函数实现的
// 这也正是所谓“协议”的接口意义!
//! \name Methods for the owner or operating system to call
//! Called when a new segment has been received from the network
void segment_received(const TCPSegment &seg);

//! Called periodically when time elapses
void tick(const size_t ms_since_last_tick);

//! \brief TCPSegments that the TCPConnection has enqueued for transmission.
//! \note The owner or operating system will dequeue these and
//! put each one into the payload of a lower-layer datagram (usually Internet datagrams (IP),
//! but could also be user datagrams (UDP) or any other kind).
std::queue<TCPSegment> &segments_out() { return _segments_out; }

//! \brief Is the connection still alive in any way?
//! \returns `true` if either stream is still running or if the TCPConnection is lingering
//! after both streams have finished (e.g. to ACK retransmissions from the peer)
bool active() const;

//! Construct a new connection from a configuration
explicit TCPConnection(const TCPConfig &cfg) : _cfg{cfg} {}

//! \name construction and destruction
//! moving is allowed; copying is disallowed; default construction not possible

~TCPConnection(); //!< destructor sends a RST if the connection is still open
TCPConnection() = delete;
TCPConnection(TCPConnection &&other) = default;
TCPConnection &operator=(TCPConnection &&other) = default;
TCPConnection(const TCPConnection &other) = delete;
TCPConnection &operator=(const TCPConnection &other) = delete;

测试程序

这部分暂时还不大明白,随便瞎写一点()

首先是socket实现,似乎要涉及到对一些事件,比如说segment receive的监听。它具体是这么做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// in libsponge/tcp_helper/tcp_sponge_socket.cc  _initialize_TCP()
// Set up the event loop

// There are four possible events to handle:
//
// 1) Incoming datagram received (needs to be given to
// TCPConnection::segment_received method)
//
// 2) Outbound bytes received from local application via a write()
// call (needs to be read from the local stream socket and
// given to TCPConnection::data_written method)
//
// 3) Incoming bytes reassembled by the TCPConnection
// (needs to be read from the inbound_stream and written
// to the local stream socket back to the application)
//
// 4) Outbound segment generated by TCP (needs to be
// given to underlying datagram socket)

比如说event4:

image-20230304171810877

什么是eventloop

事件循环(event loop)就是 任务在主线程不断进栈出栈的一个循环过程。任务会在将要执行时进入主线程,在执行完毕后会退出主线程。

这里的大致意思就是增加了一个监听事件,一旦tcp_connection的segments_out有元素,就会马上取出来

这部分不大懂,不知道后面会不会涉及对socket的编写?

还有一点是对测试脚本好像有了点了解。比如在build/CTestTestfile.cmake中可以看到每个测试的对应脚本以及使用的options:

image-20230304170132380

如果不知道option的用法可以这么做:

image-20230305232621024

这些脚本实现的对应代码在sponge/apps中。

又比如,在sponge/etc/tests.cmake中,可以找到各个测试程序执行的参数,就可以比如说修改测试的Timeout时间:

image-20230305232913611

总结:状态机

我们已经完整实现了整个TCP协议,是时候该对其做出一个总结了。

TCP协议本质上是一个状态机

在我们的sponge TCP中,我们将一个endpoint的TCP协议分成了两个状态机,一个是TCPReceiver的状态机,另一个是TCPSender的状态机。它们依据外界的输入【从app或者互联网】来进行状态的转移。

以下几张图完美地体现了状态转移关系【具体的状态体现标注在代码中了】:

image-20230226200935395

image-20230226202631406

image-20230305225738049

image-20230225232723083

TCPConnection并不是状态机,它是两个状态机和外界联通的桥梁。它的职能有:

  1. 给状态机提供输入

    包括:

    1. app调用write传进来的数据
    2. peer通过segment_received传进来的数据
  2. 处理状态机的输出

    包括:

    1. app调用receiver的stream接口获取数据
    2. 通过send_segment向peer传递数据

也可以说,它具有显式推动状态机状态转移的作用,比如说:

  1. 给状态机传递外界数据让他们转移
  2. connect通过调用fill_window推动_sender从CLOSED状态转移到SYN_SENT状态
  3. 转移到ERROR状态的条件判断

等等等。

也因而,TCPConnection并不包含复杂的逻辑和算法,它仅仅是做一些条件判断,以及一些数据转发的工作。

喜闻乐见的bug合集

相比于代码的编写,本次实验最难的部分是测试。由于lab4基于lab0-3,因而前面没有发现的bug在本次黑压压162个测试之下会全部涌现出来。有些bug我还是不知道怎么回事,并且debug过程也不像xv6那样条理清晰步步为营,感觉充满着不少玄幻色彩,所以也没有很多干货好说。在这里就先记录下印象比较深刻,耗时比较久的bug吧。

TCP produced ‘ackno=1’

image-20230303214944132

需要发送一个ackno=2的帧,但是不知道为什么却发送了一个ackno=1的,并且无论我怎么找,在哪里print,都只能找到一个ackno=2的,连1的影子都看不到。这个现象确实很诡异,但其实它的内因很简单。它是由于我对空的ACK帧发送条件限制得不恰当才出现的。

有没有觉得这里有点跳跃?我是怎么通过这个现象得知是ACK发送不恰当导致的?

答案是我当时也没想到这一点,无头苍蝇般转了可能有一个小时,这里print一下那里print一下都没有发现异常。最后我放弃了这个用例去看下一个错误的用例,才发现了这个小bug,改了一下发现这个也一起过了【绷】

本来我是这么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 把_sender的所有segment都发送出去
void TCPConnection::segment_send() {
// 当没有要发送的帧,无法进行顺带ACK时,就只能发送一个只有ACK的空帧
if (_sender.segments_out().empty()) {
if (_receiver.ackno().has_value()) {
_sender.send_empty_ack_segment(_receiver.ackno().value());
}
}
while (!_sender.segments_out().empty()) {
// ...
}
}

// in segment_received()
//if (seg.length_in_sequence_space() != 0) {
// empty_ack_send();
//}
segment_send();

如果这么写的话,当这台endpoint收到peer的一个empty ACK后,它就也会以示敬意回复一个empty ACK,这样除了本应发过去的ackno=2的报文,就多了个幽灵般的ackno=1的empty ACK,从而导致上面的错误。

因而,正确的做法是,我们在receive时只对**!empty**的seg进行ACK回复就行。具体写法可以看看我下面的代码。

超时重传时间翻倍问题

image-20230303224104016

image-20230303224053277

可以看到,它是想要我们在1000ms后再发一次FIN的,也即rto依然等于1000,但是我们的rto却是2000.为啥呢?那就去看看超时重传呗。

原来的超时重传代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// in libsponge/tcp_sender.cc
if (timer_start && ticks > timer_ticks && ticks - timer_ticks >= rto) {
// resend
if (!tmp_segments.empty()) {
_segments_out.push(tmp_segments.front().segment);
}

if (window_size != 0) {
cons_retran++;
rto *= 2;
}
timer_ticks = ticks;
}

改完后:

1
2
3
4
5
6
7
8
9
10
11
12
if (timer_start && ticks > timer_ticks && ticks - timer_ticks >= rto) {
if (!tmp_segments.empty()) {
// resend
_segments_out.push(tmp_segments.front().segment);

if (window_size != 0) {
cons_retran++;
rto *= 2;
}
}
timer_ticks = ticks;
}

想想超时重传的定义,是不是重传了之后才会double时间呀()

assembler

image-20230304163111298

这个test花了我半个下午的时间排查和修改。大致流程及报错信息是,一方发了65000个byte,但是另一方只能收到<<65000个。最后print了一下,发现是streamassembler写错了,在stream end的时候仍然有很大一部分数据未被整流。

这个直面屎山的经历极大地鼓舞了我

之前在写streamassembler的时候就知道有个地方是错的了,那就是我对capacity的理解【具体见前面的笔记】。现在只用改一下就好了。修改方式很简单,加上这两句话就行:

1
2
right = right <= left_bound + _capacity ? right : left_bound + _capacity;  // 右边越界的也不要
if (o_left >= left_bound + _capacity) goto end; // 越界的不要

t_udp_client_send超时

image-20230304215748823

这个错因非常地诡异,我到最后也还是没有自己找出来。直到我瞎搜来搜去看到了这篇文章:

我也是真的很佩服这篇文章的作者能找到这个点

image-20230304221420748

https://www.cnblogs.com/lawliet12/p/17066719.html

image-20230305215310021

噔噔咚。

我为什么不用_cfg.rt_timeout呢?答案是我当初脑子一抽以为rt_timeout是static、const的,就写了个TCPConfig::rt_timeout然后报错了,我懒得思考了就换成了上面的那个,结果……就这东西,又花费了我好久好久【悲】怪我没有认真看,没发现rt_timeout不是一个静态常量。

t_ucS_1M_32K超时

image-20230305162239877

以及其后面的其他test也都超时了。

说实话我真是百思不得其解,这里打印来那里打印去,也都看得眼花缭乱什么也看不出来,使用指导书那些手动测试的方法,还有抓包,都十分地正常,但它自动测试就是会timeout。

我折腾来折腾去,这里print那里print,最后还怀疑是电脑问题就放到服务器上跑了一下结果还是不行。绝望之际,我只能使出了万策尽之时的迫不得已的非法手段:将我的一部分代码替换成别人的看看会怎么样。【传统艺能23333】

最终我定位发现是TCPSender出了问题,我猜测是因为状态机出错了。我比对着别人的代码【知道这不对,但我心态已经崩了。。。】,以及指导书提供的状态机,发现是这个地方出了小问题:

image-20230305220614819

1
2
3
4
5
6
7
8
9
// in tcp_sender.cc fill_window()
// 注释的是以前写的错误版本
// if (_stream.input_ended() && !fin && remaining > 0) {
if (_stream.eof() && !fin && remaining > 0) {
// last segment
segment.header().fin = true;
fin = true;
remaining -= 1;
}

这里不应该是input_ended,而应该是eof……

改了之后立刻所有测试都能跑通了【悲】

那么问题来了,为什么错误版本就会timeout呢?我的猜测如下:

eof的条件如下:

1
bool ByteStream::eof() const { return is_input_end && buffer.empty(); }

可以看到,eof既要求input_ended,又要求缓冲区内所有数据成功发送。这也很符合FIN_SENT的语义:在数据流终止时(所有数据成功发送,不要求fully acked)发送FIN。

如果按照我错误版本的写法,会导致数据还没发送完毕(!buffer.empty()),就发送了FIN。之后数据虽然还能正常进入receiver的bytestream,并且发送给peer的receiver。但是会存在这也一个空窗期:FIN之后的数据还没到的时候,peer的receiver接收到FIN,并且peer的app从socket将receiver接收到的数据全部读出。出现了这样的空窗期,就会导致peer的receiver的stream达到eof状态:

1
2
3
4
5
6
7
// in tcp_receiver.cc segment_received()
if (abs_seqno != 0)
_reassembler.push_substring(data, index, header.fin);
// in streamassembler.cc
if (is_eof && buffer.empty()) {
_output.end_input();
}

【接下来就是猜了】由于bytestream eof了,socket就停止读了。后来的数据再来,receiver的stream的缓冲区就满了,receiver就只能一直丢包。【接下来是真的纯猜】而且由于测试脚本问题,在这之后都不会调用tick方法了,故而超时重传检测不会被触发,而sender也会因为没有ack,而一直重传重传,就死循环然后timeout寄掉了。

纯猜部分的依据是:

image-20230305173110776

image-20230305173152469

可以看到,tick方法一直被调用,但是ticks却不变。数据报文一直被重传,但是retran一直不变。ticks-timer_ticks一直大于rto,但却始终无法进入那句if(经测试是这样的)。这非常奇怪,我也不知道为什么。

代码

【珍贵的调试用代码没删的版本放在github了。】

TCPConnection.hh

1
2
3
4
5
6
7
8
9
10
class TCPConnection {
private:
// ...
size_t rec_tick{};// 上一次收到segment时的ticks数
size_t ticks = 0;
public:
void segment_send();
void empty_ack_send();
void set_rst();
// ...

TCPConnection.cc

如果想要以状态机的视角来看待,可以看看感恩的代码。他写得很清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#include "tcp_connection.hh"
#include <iostream>

template <typename... Targs>
void DUMMY_CODE(Targs &&... /* unused */) {}

using namespace std;

size_t TCPConnection::remaining_outbound_capacity() const { return _sender.stream_in().remaining_capacity(); }

size_t TCPConnection::bytes_in_flight() const { return _sender.bytes_in_flight(); }

size_t TCPConnection::unassembled_bytes() const { return _receiver.unassembled_bytes(); }

size_t TCPConnection::time_since_last_segment_received() const { return ticks - rec_tick; }

// 发送一个只有ACK的空帧,仅在segment_received中调用
// 当没有要发送的帧,无法进行顺带ACK时,
// 为了保障一定有ACK发送,就只能发送一个只有ACK的空帧
void TCPConnection::empty_ack_send() {
if (_sender.segments_out().empty()) {
if (_receiver.ackno().has_value()) {
_sender.send_empty_ack_segment(_receiver.ackno().value());
}
}
}

// 把_sender的所有segment都发送出去
void TCPConnection::segment_send() {
while (!_sender.segments_out().empty()) {
TCPSegment seg = _sender.segments_out().front();
// 顺带ACK
if (_receiver.ackno().has_value()) {
seg.header().ack = true;
seg.header().ackno = _receiver.ackno().value();
}
seg.header().win = _receiver.window_size();
_segments_out.push(seg);
_sender.segments_out().pop();
}
}

// connection被置为error状态的部分必要操作
void TCPConnection::set_rst() {
_sender.stream_in().set_error();
_receiver.stream_out().set_error();
_linger_after_streams_finish = false;
}

void TCPConnection::segment_received(const TCPSegment &seg) {
// 重置发送的ticks
rec_tick = ticks;

if (seg.header().rst) {
// RST is set
set_rst();
return;
}

// 回复对方问你是死是活的信息
if (_receiver.ackno().has_value() && seg.length_in_sequence_space() == 0 &&
seg.header().seqno - _receiver.ackno().value() < 0) {
_sender.send_empty_segment();
segment_send();
return;
}

_receiver.segment_received(seg);

if (seg.header().ack) { // ack_received也会调用fill_window
_sender.ack_received(seg.header().ackno, seg.header().win);
} else
_sender.fill_window();

// 只在本次收到的seg需要被ACK的时候才要ACK。
// 需要被ACK:FIN/SYN/携带数据 总之就是length!=0
// 不得不说,FIN和SYN都会占一个序列号这个点给ACK设计带来了简便,同时也增加了安全性
if (seg.length_in_sequence_space() != 0) {
empty_ack_send();
}

segment_send();

// If the inbound stream ends before the TCPConnection has reached EOF
// on its outbound stream, this variable needs to be set to false
// 如果receiver的那个stream比sender的stream早结束,就不用等待
// 为什么呢?因为receiver的stream结束说明了全部的seg都成功接收并且全部整流【参见assembler实现】
// 也就说明对方不发送数据了,并且已经把FIN也发过来了
// 也即对方进入了FIN_WAIT状态
// 而我们的sender还在输出,也即我们在CLOSE_WAIT状态
// 因而我们只需输出完剩余数据再发送AF,最后直接关闭就行
// 因为我们知道对方已经关闭了,无需再进行linger。
if (_receiver.stream_out().input_ended() && !_sender.stream_in().eof()) {
// peer:FIN_WAIT self:CLOSE_WAIT
_linger_after_streams_finish = false;
}
}

bool TCPConnection::active() const{
// 处于error状态
if (!_linger_after_streams_finish && _receiver.stream_out().error() && _sender.stream_in().error()) {
return false;
}

// 满足条件1-3
if (_receiver.stream_out().input_ended() &&
_sender.stream_in().eof() && _sender.bytes_in_flight() == 0 && _sender.fully_acked()) {
// 无需等待的话就直接返回false
if (!_linger_after_streams_finish)
return false;
// 否则需要等待10*timeout
else if (time_since_last_segment_received() >= 10 * _cfg.rt_timeout){
return false;
}
}
return true;
}

size_t TCPConnection::write(const string &data) {
size_t res = _sender.stream_in().write(data);
// 注意此处需要手动调一下fill_window和send方法
_sender.fill_window();
segment_send();
return res;
}

// ms_since_last_tick: number of milliseconds since the last call to this method
void TCPConnection::tick(const size_t ms_since_last_tick) {
ticks += ms_since_last_tick;
_sender.tick(ms_since_last_tick);
if (_sender.consecutive_retransmissions() > _cfg.MAX_RETX_ATTEMPTS) {
while (!_sender.segments_out().empty())
_sender.segments_out().pop(); // 清除sender遗留的所有帧
_sender.send_empty_rst_segment();// 只发送rst帧
set_rst();
}
segment_send();

// end the connection cleanly if necessary
if (_receiver.stream_out().input_ended() &&
_sender.stream_in().eof() && _sender.bytes_in_flight() == 0 && _sender.fully_acked()
&& time_since_last_segment_received() >= 10 * _cfg.rt_timeout) {
// 等待结束
_linger_after_streams_finish = false;
}

}

void TCPConnection::end_input_stream() {
_sender.stream_in().end_input();
_sender.fill_window();
segment_send();
}

void TCPConnection::connect() {
_sender.fill_window();
// send_segment重复代码。目的是防止发送SYN外还发送别的东西
if (!_sender.segments_out().empty()) {
TCPSegment seg = _sender.segments_out().front();
if (_receiver.ackno().has_value()) {
seg.header().ack = true;
seg.header().ackno = _receiver.ackno().value();
}
seg.header().win = _receiver.window_size();
_segments_out.push(seg);
_sender.segments_out().pop();
}
}

TCPConnection::~TCPConnection() {
try {
// shutdown uncleanly
if (active()) {
set_rst();
_sender.send_empty_rst_segment();
segment_send();
}
} catch (const exception &e) {
std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
}
}

debug函数

1
2
3
4
5
6
// in libsponge/tcp_helper/tcp_segment.hh
void print_seg() const{
std::cerr<<" flag="<<(header().syn?"S":"")<<(header().ack?"A":"")<<(header().fin?"F":"")
<<" seqno="<<header().seqno.raw_value()<<" ackno="<<header().ackno.raw_value()
<<" payload_size:"<<payload().size()<<std::endl;
}

in libsponge/tcp_helper/fd_adapter.cc

image-20230304172207234