# 实验二

## 🧱 前情提要

本人没有了解arm系统的文件会有什么问题，这里只提供x86的实验建议。

~~实验二的实验文档疑似太抽象了~~

## 🛠 实验文档补充

由于实验文档乱七八糟的，这里重述一下实验要求。

从实验要实现的功能来说：

3.1：要实现server端和client端的三次握手和四次挥手

3.2:在3.1的基础上，实现短消息的收发。

代码难度上，3.1 80%，3.2 20%（建立在你理解了3.1的基础上）。

然后就是这堆乱七八糟的代码干了啥。先说3.1。

首先，你要先了解三次握手和四次挥手的理论知识。~~那张TCP状态转移图很形象，可惜做完了才看懂~~

关于理论知识以及一堆.h里定义的轮子是干什么的可以参考<https://www.yuque.com/u37222421/gh80t7/ktxtl7cfq4vkgaz9?singleDoc#>

这位同学的原理讲述很详细（~~他的偏商务，我的偏运动~~）

然后我们要知道这堆代码是怎么跑起来的。

````
```c
int main(int argc, char **argv)
{
	setbuf(stdout,NULL);
	
	if (getuid() && geteuid()) {
		fprintf(stderr, "Permission denied, should be superuser!\n");
		exit(1);
	}

	if (argc < 2) {
		usage_and_exit(argv[0]);
	}

	init_ustack();

	arpcache_init();

	init_rtable();
	load_rtable_from_kernel();

	init_tcp_stack();

	run_application((const char *)basename(argv[0]), argv+1, argc-1);

	ustack_run();

	return 0;
}
```
````

这是main函数，我们在3.1只要关心它调用了run\_application就行（~~其调用方式可以当做操作系统的小知识点~~）。

那么run\_application又干了什么？

````
```c
static void run_application(const char *basename, char **args, int n)
{
	pthread_t thread;

	if (strcmp(args[0], "server") == 0) {
		if (n != 2)
			usage_and_exit(basename);

		u16 port = htons(atoi(args[1]));
		pthread_create(&thread, NULL, tcp_server, &port);
	}
	else if (strcmp(args[0], "client") == 0) {
		if (n != 3)
			usage_and_exit(basename);

		struct sock_addr skaddr;
		skaddr.ip = inet_addr(args[1]);
		skaddr.port = htons(atoi(args[2]));
		pthread_create(&thread, NULL, tcp_client, &skaddr);
	}
	else {
		usage_and_exit(basename);
	}
}
```
````

它创造了两个线程，根据参数决定执行tcp\_client还是tcp\_server。

那我们看看tcp\_client和tcp\_server干了啥。（在tcp\_apps.c）

````
```c
void *tcp_server(void *arg)
{
	u16 port = *(u16 *)arg;
	struct tcp_sock *tsk = alloc_tcp_sock();

	struct sock_addr addr;
	addr.ip = htonl(0);
	addr.port = port;
	if (tcp_sock_bind(tsk, &addr) < 0) {
		log(ERROR, "tcp_sock bind to port %hu failed", ntohs(port));
		exit(1);
	}

	if (tcp_sock_listen(tsk, 3) < 0) {
		log(ERROR, "tcp_sock listen failed");
		exit(1);
	}

	log(DEBUG, "listen to port %hu.", ntohs(port));

       struct tcp_sock *csk = tcp_sock_accept(tsk);

	log(DEBUG, "accept a connection.");
	sleep(5);
	tcp_sock_close(csk);
	return NULL;
}

// tcp client application, connects to server (ip:port specified by arg), each
// time sends one bulk of data and receives one bulk of data 
void *tcp_client(void *arg)
{
	struct sock_addr *skaddr = arg;

	struct tcp_sock *tsk = alloc_tcp_sock();

	if (tcp_sock_connect(tsk, skaddr) < 0) {
		log(ERROR, "tcp_sock connect to server ("IP_FMT":%hu)failed.", \
				NET_IP_FMT_STR(skaddr->ip), ntohs(skaddr->port));
		exit(1);
	}
	log(DEBUG,"tcp_sock connect to server ("IP_FMT":%hu) succeeded.", \
			NET_IP_FMT_STR(skaddr->ip), ntohs(skaddr->port));//自己加的
	
   sleep(1);
	tcp_sock_close(tsk);
	
	return NULL;
}
```
````

可以看出，client调用了tcp\_sock\_connect函数，server调用了tcp\_sock\_accept函数，alloc\_tcp\_sock和tcp\_sock\_close函数二者都用。alloc\_tcp\_sock已经写好了，就是造个socket，你的free\_tcp\_sock应该把alloc\_tcp\_sock函数malloc的所有东西都free掉。（注意，为了防止double free(内存被多次释放)，建议在freee\_tcp\_sock实现时加上出错处理）。

然后你发现tcp\_sock\_connect,tcp\_sock\_accept,tcp\_sock\_close都要你实现怎么办呢？还有一堆不知道在干什么的函数要你实现？（~~吐槽一下实验文档的任务要求里都忘记写tcp\_sock\_accept函数了~~）

我们还是分析要干什么。这里先给出一些辅助函数要干什么的提示。（看tcp\_sock.c）

tcp\_set\_state:不要你写，但是要记住调用这个就会改变状态（状态即实验文档里的TCP状态），改变状态干什么呢？教材说过，看书。（实验里要通过状态来做些事，后面说）

Init\_tcp\_stack:初始化hash表，不用你调用。

alloc和free前面说过了。

tcp\_sock\_lookup系列：先看懂实验文档里的SOCKET与元组信息的绑定，然后看注释即可实现。请严格按照3.1.3函数要求来实现，比如tcp\_sock\_lookup\_listen使用tcp\_hash\_function。

tcp\_bind\_hash:你要实现的部分用不上（也不应该调用！）

tcp\_hash系列：这才是你要用的函数，具体怎么用后面说

enqueue/dequeue：同理，后面说

轮子函数说完了，我们来到关键函数的实现。

首先是这个函数tcp\_send\_control\_packet。（注意：在三次握手和四次挥手过程，你不应该调用tcp\_send\_packet函数，而应该调用这个函数）

```c
void tcp_send_control_packet(struct tcp_sock *tsk, u8 flags)
{
	int pkt_size = ETHER_HDR_SIZE + IP_BASE_HDR_SIZE + TCP_BASE_HDR_SIZE;
	char *packet = malloc(pkt_size);
	if (!packet) {
		log(ERROR, "malloc tcp control packet failed.");
		return ;
	}

	struct iphdr *ip = packet_to_ip_hdr(packet);
	struct tcphdr *tcp = (struct tcphdr *)((char *)ip + IP_BASE_HDR_SIZE);

	u16 tot_len = IP_BASE_HDR_SIZE + TCP_BASE_HDR_SIZE;

	ip_init_hdr(ip, tsk->sk_sip, tsk->sk_dip, tot_len, IPPROTO_TCP);
	tcp_init_hdr(tcp, tsk->sk_sport, tsk->sk_dport, tsk->snd_nxt, \
			tsk->rcv_nxt, flags, tsk->rcv_wnd);

	tcp->checksum = tcp_checksum(ip, tcp);

	if (flags & (TCP_SYN|TCP_FIN))
		tsk->snd_nxt += 1;

	ip_send_packet(packet, pkt_size);
}
```

**代码和实验文档里都没有说它的调用路径（实验文档那个发送与接收的函数调用链是3.2的，不知道为什么放3.1里）,但是一个很关键的点是，你通过这个函数每发一个包给对端，它最终都会调用tcp\_process函数处理这个包！这就是你为啥要实现tcp\_process函数！**

所以三次握手其实是tcp\_sock\_connect/tcp\_sock\_accept函数和tcp\_process联手合作的结果。

那么四次挥手呢？其实是tcp\_sock\_close和tcp\_process联手合作的结果。

现在在我们谈谈怎么合作之前，还要了解一个重要函数：sleep\_on()和wake\_up()。

```c
// sleep on waiting for notification
static inline int sleep_on(struct synch_wait *wait)
{
	pthread_mutex_lock(&wait->lock);
	if (wait->dead)
		goto unlock;
	wait->sleep = 1;
	if (!wait->notified)
		pthread_cond_wait(&wait->cond, &wait->lock);
	wait->notified = 0;
	wait->sleep = 0;
unlock:
	pthread_mutex_unlock(&wait->lock);

	return -(wait->dead);
}

// notify others
static inline int wake_up(struct synch_wait *wait)
{
	pthread_mutex_lock(&wait->lock);
	if (wait->dead)
		goto unlock;

	if (!wait->notified) {
		wait->notified = 1;
		if (wait->sleep)
			pthread_cond_signal(&wait->cond);
	}

unlock:
	pthread_mutex_unlock(&wait->lock);
	return -(wait->dead);
}
```

这是一个多线程函数，简单理解就是，你调用sleep\_on就会睡着，代码停止执行，只有在其他某处调用了wake\_up才会继续执行接下来的代码（多线程经典案例），相同的wait参数是一一对应的。

所以说你现在就知道怎么等包了：connect/accept函数发包后sleep\_on，等收到包了，tcp\_process函数被调用，在tcp\_process函数里wake\_up就行了。（从注释来说，connect/accept函数应该只参与刚开始的发包，后面的发包都是tcp\_process处理，但是你完全可以也写在connect/accept里，实现很灵活）

综上所述，我们得出了一个基本思路：

根据三次握手和四次挥手的过程，正确将上面说的函数进行合作，通过sleep\_on和wake\_up的运用实现。

但是还有许多小细节需要处理，下面以问答的方式给出：

**1.connect发不出第一个SYN包，出现could not find forwarding rule for IP (dst:0.0.0.0) packet。**

从结果上是因为tsk->sk\_sip设置有问题，请使用longest\_prefix\_match来解决问题。但是想发包还要注意以下几点：

1.tsk->sk\_dip和sk\_dport需要转字节序。

2.必须调用tcp\_sock\_set\_sport初始化！

3.使用 longest\_prefix\_match 查找本机 IP 地址，详见实验手册。

4.状态先设置，然后必须hash进去（为什么？因为匹配必须在hash表里，你改东西了不重新hash它可不知道你改了）！那为什么说“状态先设置”?

````c
   ```c
tcp_set_state(tsk, TCP_SYN_SENT);
    tcp_hash(tsk);
```
```c
tcp_hash(tsk);
tcp_set_state(tsk,TCP_SYN_SENT)；
```
````

上面的是对的，下面的是错的（如果初始为TCP\_CLOSED状态），**看看tcp\_hash的实现就知道为啥了!**

**2.tcp\_sock\_accept函数注释说的accept\_queue是干嘛的？**

如果接受队列为空，则等待连接请求,如果非空，执行tsk=tcp\_sock\_accept\_dequeue(tsk);。

但是要注意的是，有dequeue就肯定在某处先调用了enqueue(这也是“我的发包在SYN\_SENT卡住了”的一个可能原因)，我这里是在tcp\_process里调用的。

这里给出我这里的一个示例tcp\_process的部分实现给大家参考（部分代码用注释代替）

<pre class="language-c"><code class="lang-c">switch (tsk->state) {
        case TCP_LISTEN:
            if (cb->flags &#x26; TCP_SYN) {//如果状态为TCP_LISTEN且收到SYN包
                //log(DEBUG,"get SYN");
                //设置tsk的sip.sport,dip,dport,新序列号(tcp_new_iss)，定义请看实验文档 
<strong>                // 设置接收下一个序列号
</strong><strong>                //绑定设置parent
</strong>                tcp_set_state(tsk, TCP_SYN_RECV);
                //hash tsk
                tcp_send_control_packet(tsk, TCP_SYN | TCP_ACK);
                tcp_sock_accept_enqueue(tsk);
                wake_up(tsk->wait_accept);
            }
            break;
            
</code></pre>

注意了，在tcp\_process里，实验文档说的**tcp收发序列号一定要设置对**，要不还是收不到包的。

**3.我的状态卡住了/状态转移不对/double free了？**

首先，一定是你tcp\_process和tcp\_sock.c里的那三个函数没有正确合作，我的建议是打log,你可以在任意地方打，用log追踪执行过程是否符合三次握手/四次挥手的过程。

你的Log没有实验预期结果的某些debug信息是正常的，你只要保证switch\_state的log是完全符合实验手册的预期结果就行。

状态卡住了80%可能性都是sleep\_on没被wake\_up唤醒，查有没有调用你想要的wake\_up就知道了。（请用正确的synch \*wait避免线程冲突!）

状态转移不对可能性很多，但本质都是函数调用过程不是你想的那样，打log。

double free了说明你调用了多次free\_tcp\_sock,可能有两个原因：

1.tcp\_unhash里有调用了free\_tcp\_sock,然后你又显式调用了一次。（~~很隐蔽，不是吗？~~）

2.你的计时器timer.c没写对，线程一直调用free爆炸了。（或者你的计时器被set了多个，我干过）

关于计时器补充一个其他人遇到的问题，其实就是1说的，tcp\_bind\_unhash和tcp\_hash的实现你看一下会发现他们都调用了free\_tcp\_sock和list\_delete\_entry,不要多次free,不然seg fault或者Double free报错都有可能。

~~如果你没有信心写对多线程计时器，喂给ai~~,但是记得要加锁！加锁！加锁！

以及，加锁请初始化锁！初始化锁！初始化锁！

**4.关于close函数**

close函数被client和server都调用（client先server后，这也符合四次挥手的情况，实现很暴力，server sleep的时间比client长）,所以一定要规划好状态的变化以及client和server端都会怎么走,很容易出错。

这里也涉及什么时候set\_tcp\_timer的问题，一定要想好。（实现方法会有很多，符合四次挥手的都是对的）

**5.关于测试样例**

测试样例有个很坑的点（~~其实实验文档说了，但是你不可能很快看出来~~）

第一个就是双方互相调用你写的代码，但是你写的双方能通信不代表你写的就是完全符合协议标准的，所以第二个和第三个实际上是用**一端绝对正确的收发包逻辑来测试你的逻辑是否符合要求。**

所以如果你第一个样例双方./tcp\_stack状态是对的而后面的不对，说明你实现“错了”。

但有时又没有完全错，因为**四次挥手python的协议栈把FIN和ACK合包了！** &#x20;

而你的tcp\_process很可能没有处理cb->flags&(TCP\_FIN|TCP\_ACK)该怎么状态转换，这和单包是不一样的。

所以记得要处理欧\~（~~没想到吧，实验手册3.1.2.9其实说了，但是它没说什么时候考虑~~）

***

如果你3.1的状态转移完全正确，那么你就可以开始做3.2了。

3.2比较简单（~~ai能写对80%~~）,只要补两个函数和改一下tcp\_app.c里的两个函数就行。

大体思路就是tcp\_client和tcp\_server调用tcp\_sock\_write和tcp\_sock\_read函数来实现收发，去掉sleep(5)和sleep(1)。

这时候你就可以回去看实验手册里“发送与接收的函数调用链”那一节了。（~~注意接收侧你要倒着从下往上看才是调用顺序，有意思吧？,发送侧是从上往下看~~）

然后你还要搞懂ring\_buffer是怎么回事，buffer不是凭空出现的，你要写进去一定也是tcp\_process干的，所以你要补上这段处理逻辑。

还有窗口大小问题，你有没有注意到tcp\_in.c(tcp\_process函数所处位置)里还有tcp\_update\_window和tcp\_update\_window\_safe两个函数？这个就是用来给你更新滑动窗口size的函数，记得在三次握手时补上，ring\_buffer写入时也要更新window。

这里还是要用sleep\_on和wake\_up实现tcp\_sock\_write/read和tcp\_process的交互，具体请自己思考。

Data怎么发送是一个工程实现问题，请自行思考（~~ai能写对~~）。

最后，**别忘了收到包要回复ACK包**！

测试和3.1几乎一样，只是python测试用的是tcp\_stack\_trans.py了，并且似乎有bug,如果你server端执行py,client执行tcp\_stack,四次挥手时卡在了FIN\_WAIT\_1状态是正常的。（关于这个事情，详见最后“一个神奇的现象”）

***

## 关于OJ测什么

OJ测的不严，只关心你的server echos对不对(记得加空格，详见实验文档)，还有一些小的状态，所以其实你三次握手和四次挥手不完全对理论上也可能混过去（~~或者面向样例编程，不过那样你实验三就没得做了~~）。

***

## 一个神奇的现象

如果你的server端测试tcp\_sock\_trans.py,client端测试tcp\_stack,你可能会遇到如下现象：我明明client端挥手给server端发了fin包，为什么它不理我，不给我发回FIN|ACK包？

这个问题从解决的角度，只要把发的FIN包改为FIN|ACK包就可以被应答了，但是为什么要加一个ACK？这个ACK是给谁的？

要明白这个，首先要知道TCP传输层是全双工的。（可以看<https://blog.csdn.net/qq_44663029/article/details/120524798>）

所以链接建立后每个包都应该带ack，发自己的信息同时也要回应对方，虽然对方可能暂时啥都没发，为了做到双方同时的包传输。

并且，fin标识很多时候是贴在最后一个数据报文上的。

所以实验三那句话的意思不仅是在说server端发包要发FIN|ACK，也是在说client端主动关闭。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://blueroaring-njus-organization.gitbook.io/njucs_25spring_network-exp_additional/zheng-pian/shi-yan-er.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
