send和recv函数

在之前的学习中,我们在不少示例中用到send和recv这两个函数,但一直没有详细解释过着两个函数中每个参数的含义。本节将介绍Linux平台下的send&recv函数

#include <sys/socket.h>
ssize_t recv(int sockfd, const void *buf, size_t nbytes, int flags);

  

  • sockfd:表示与数据传输对象的连接的套接字文件描述符
  • buf:保存待传输数据的缓冲地址值
  • nbytes:待传输字节数
  • flags:传输数据时指定的可选项信息
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);

  

  • sockfd:表示与数据接收对象的连接的套接字文件描述符
  • buf:保存接收数据的缓冲地址值
  • nbytes:可接收最大字节数
  • flags:接收数据时指定的可选项信息

send函数和recv函数的最后一个参数是收发数据时的可选项,该可选项可利用位(bit)或运算符同时传递多个信息,通过表1-1整理可选项的种类及含义

表1-1   send&recv函数的可选项及含义
可选项(option) 含义 send recv
MSG_OOB 用于传输外带数据(Out-of-band data)
MSG_PEEK 验证输入缓冲中是否存在接收的数据  
MSG_DONTROUTE 数据传输过程中不参照路由(Routing)表,在本地(Local)网络中寻找目的地  
MSG_DONTWAIT 调用I/O函数时不阻塞,用于使用非阻塞(Non-blocking)I/O
MSG_WAITALL 防止函数返回,直到接收全部请求的字节数  

另外,不同操作系统对上述可选项的支持也不同。因此,为了使用不同可选项,需对实际开发采用的操作系统有一定了解。下面截取表1-1中的一部分(主要是不受操作系统差异影响的)进行讲解

MSG_OOB:发送紧急消息

MSG_OOB可选项用于发送“外带数据”紧急消息,好比医院有很多病人排队等着看病,现在突然来了个急诊患者该怎么办?当然是优先处理了。MSG_OOB可选项就用于创建特殊发送方法和通道以发送紧急消息,下面示例将通过MSG_OOB可选项收发数据

oob_send.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h> #define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in recv_adr;
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
} sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&recv_adr, 0, sizeof(recv_adr));
recv_adr.sin_family = AF_INET;
recv_adr.sin_addr.s_addr = inet_addr(argv[1]);
recv_adr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *)&recv_adr, sizeof(recv_adr)) == -1)
error_handling("connect() error!"); write(sock, "123", strlen("123"));
send(sock, "4", strlen("4"), MSG_OOB);
write(sock, "567", strlen("567"));
send(sock, "890", strlen("890"), MSG_OOB);
close(sock);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

  • 第29~32行:传输数据,第30行和第32行紧急传输数据。正常顺序应该是123、4、567、890,但紧急传输了4和890,由此可知接收顺序也将改变

从上面示例可以看出,紧急消息的传输比即将介绍的接收过程要简单,只需在调用send函数时指定MSG_OOB可选项,接收紧急消息的过程要相对复杂一点

oob_recv.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h> #define BUF_SIZE 30
void error_handling(char *message);
void urg_handler(int signo); int acpt_sock;
int recv_sock; int main(int argc, char *argv[])
{
struct sockaddr_in recv_adr, serv_adr;
int str_len, state;
socklen_t serv_adr_sz;
struct sigaction act;
char buf[BUF_SIZE];
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
} act.sa_handler = urg_handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0; acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&recv_adr, 0, sizeof(recv_adr));
recv_adr.sin_family = AF_INET;
recv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
recv_adr.sin_port = htons(atoi(argv[1])); if (bind(acpt_sock, (struct sockaddr *)&recv_adr, sizeof(recv_adr)) == -1)
error_handling("bind() error");
listen(acpt_sock, 5); serv_adr_sz = sizeof(serv_adr);
recv_sock = accept(acpt_sock, (struct sockaddr *)&serv_adr, &serv_adr_sz); fcntl(recv_sock, F_SETOWN, getpid());
state = sigaction(SIGURG, &act, 0); while ((str_len = recv(recv_sock, buf, sizeof(buf), 0)) != 0)
{
if (str_len == -1)
continue;
buf[str_len] = 0;
puts(buf);
}
close(recv_sock);
close(acpt_sock);
return 0;
} void urg_handler(int signo)
{
int str_len;
char buf[BUF_SIZE];
str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_OOB);
buf[str_len] = 0;
printf("Urgent message: %s \n", buf);
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

  • 第29、47行:该示例中需要重点观察SIGURG信号相关部分,收到MSG_OOB紧急信号时,该信号将产生SIGURG信号,并调用注册的信号处理函数。另外需要注意的是,第61行的信号处理函数内部调用了紧急消息的recv函数
  • 第46行:调用fcntl函数,此函数将单独说明

上述示例中插入了未曾讲解的fcntl函数调用语句,关于此函数只讲解必要部分,后面还会再讲解此函数

fcntl(recv_sock, F_SETOWN, getpid());

  

fntcl函数用于控制文件描述符,但上述调用语句的含义为:将文件描述符recv_sock指向的套接字拥有者(F_SETOWN)改为把getpid函数返回值用作ID进程。可能大家对“套接字拥有者”的概念有些生疏,操作系统实际上创建管理套接字,所以从严格意义上说,操作系统才是套接字的拥有者,只是此处所谓的“拥有者”是指套接字所有事物的主体,上述描述可简要概括为:文件描述符recv_sock指向的套接字引发的SIGURG信号处理进程变为将getpid函数返回值用作ID的进程

当然,上述描述中的“处理SIGURG信号”指的是“调用SIGURG信号处理函数”。但之前讲过,多进程可以共同拥有一个套接字描述符。例如,通过调用fork函数创建子进程并同时复制文件描述符。此时如果发生SIGURG信号,应该调用哪个进程的信号处理函数呢?可以肯定的是,不会调用所有进程的信号处理函数。因此,处理SIGURG信号时必须指定处理信号的进程,而getpid函数返回调用此函数的进程ID。上述调用语句指定当前进程为处理SIGURG信号的主体。该程序只创建一个进程,因此,理应由该进程处理SIGURG信号

编译oob_recv.c并运行

# gcc oob_recv.c -o oob_recv
# ./oob_recv 8500
Urgent message: 4
Urgent message: 0
123
56789

  

编译oob_send.c并运行

# gcc oob_send.c -o oob_send
# ./oob_send 127.0.0.1 8500

  

输出结果可能出乎大家的意料,通过MSG_OOB可选项传递数据时只返回一个字节?而且速度也不是很快。的确,通过MSG_OOB可选项传输数据时不会加快传输速度,而且通过信号处理函数urg_handler读取数据时也只能读取一个字节。剩余数据只能通过未设置MSG_OOB可选项的普通输入函数读取,这是因为TCP不存在真正意义上的“外带数据”。实际上,MSG_OOB中的OOB是指Out-of-band,而“外带数据是”:通过完全不同的通信路径传输的数据。

即真正意义上的Out-of-band需要通过单独的通信路径提高传输数据,但TCP不提供,只利用TCP的紧急模式进行传输。

MSG_OOB真正的意义在于督促数据接收对象尽快处理数据,这是紧急模式的全部内容,而且TCP“保持传输顺序”的传输特性依然成立。那怎么能称为紧急消息呢?这确实是紧急消息!因为发送消息者是在催促处理的情况下传输数据,像之前举的医院的例子,急诊患者的及时救治需要两个条件:(1)迅速入院;(2)医院急救;无法快速把病人送到医院,并不意味着不需要医院进行急救。TCP的紧急消息无法保证及时入院,但可以要求急救。当然,急救措施应由程序员完成。之前的示例oob_recv.c的运行过程中也传递了紧急消息,这可以通过事件处理函数确认,这就是MSG_OOB模式数据传输的实际意义。下面给出设置MSG_OOB可选项状态下的数据传输过程,如图1-1所示

图1-1   紧急消息传输阶段的输出缓冲

图1-1给出的是示例oob_send.c的第32行中调用如下函数后的输出缓冲状态,此处假设已传输之前的数据

send(sock, "890", strlen("890"), MSG_OOB);

  

如果将缓冲最左端的位置视作偏移量为0,字符0保存于偏移量为2的位置。另外,字符0右侧偏移量为3的位置存有紧急指针。紧急指针指向紧急消息的下一个位置(偏移量加1),同时向对方主机传递消息:紧急指针指向的偏移量为3之前的部分就是紧急消息。也就是说,实际只用一个字节表示紧急消息。这一点可以通过图1-1中用于传输数据的TCP数据包(段)的结构看的更清楚,如图1-2所示

图1-2   设置URG的数据包

TCP数据包实际包含很多信息,图1-2只标注了与我们主题相关的内容,TCP头含有如下两种信息:

  • URG=1:载有紧急消息的数据包
  • URG指针:紧急指针位于偏移量为3的位置

指定MSG_OOB选项的数据包本身就是紧急数据包,并通过紧急指针表示紧急消息所在位置,但通过图1-2无法得知紧急消息是字符串890?还是90?亦或是单个0?但这并不重要,如前所述,除紧急指针的前面一个字节外,数据接收方将通过调用常用输入函数读取剩余部分。换言之,紧急消息的意义在于督促消息处理,而非紧急传输形式受限的消息

检查输入缓冲

同时设置MSG_PEEK选项和MSG_DONTWAIT选项,以验证输入缓冲中是否存在接收的数据。设置MSG_PEEK选项并调用recv函数时,即使读取了输入缓冲的数据也不会删除。因此,该选项通常与MSG_DONTWAIT合作,用于调用以非阻塞方式验证待读数据存在与否的函数

peek_send.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
void error_handling(char *message); int main(int argc, char *argv[])
{
int sock;
struct sockaddr_in send_adr;
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
} sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&send_adr, 0, sizeof(send_adr));
send_adr.sin_family = AF_INET;
send_adr.sin_addr.s_addr = inet_addr(argv[1]);
send_adr.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *)&send_adr, sizeof(send_adr)) == -1)
error_handling("connect() error!"); write(sock, "123", strlen("123"));
close(sock);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

上述示例第24行发起连接请求,第27行发送字符串123。下面示例给出了使用MSG_PEEK选项和MSG_DONTWAIT选项的结果

peek_recv.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h> #define BUF_SIZE 30
void error_handling(char *message); int main(int argc, char *argv[])
{
int acpt_sock, recv_sock;
struct sockaddr_in acpt_adr, recv_adr;
int str_len, state;
socklen_t recv_adr_sz;
char buf[BUF_SIZE];
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
} acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&acpt_adr, 0, sizeof(acpt_adr));
acpt_adr.sin_family = AF_INET;
acpt_adr.sin_addr.s_addr = htonl(INADDR_ANY);
acpt_adr.sin_port = htons(atoi(argv[1])); if (bind(acpt_sock, (struct sockaddr *)&acpt_adr, sizeof(acpt_adr)) == -1)
error_handling("bind() error");
listen(acpt_sock, 5); recv_adr_sz = sizeof(recv_adr);
recv_sock = accept(acpt_sock, (struct sockaddr *)&recv_adr, &recv_adr_sz); while (1)
{
str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK | MSG_DONTWAIT);
if (str_len > 0)
break;
} buf[str_len] = 0;
printf("Buffering %d bytes: %s \n", str_len, buf); str_len = recv(recv_sock, buf, sizeof(buf) - 1, 0);
buf[str_len] = 0;
printf("Read again: %s \n", buf);
close(acpt_sock);
close(recv_sock);
return 0;
} void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

  

  • 第38行:调用recv函数的同时传递MSG_PEEK可选项,这是为了保证即使不存在待读取数据也不会进入阻塞状态
  • 再次调用recv函数,这次并未设置任何可选项。因此,本次读取的数据将从输入缓冲中删除

编译peek_recv.c并运行

# gcc peek_recv.c -o peek_recv
# ./peek_recv 8500
Buffering 3 bytes: 123
Read again: 123

  

编译peek_send.c并运行

# gcc peek_send.c -o peek_send
# ./peek_send 127.0.0.1 8500

  

通过运行结果可以验证,仅发送一次的数据被读取两次,因为第一次调用recv函数时设置了MSG_PEEK选项,以上就是MSG_PEEK可选项的功能

readv和writev函数

本节介绍的readv和writev函数有助于提高数据通信效率,先介绍这些函数的使用方法,再讨论其合理的应用场景。readv和writev函数的功能可概括为:对数据进行整合传输及发送的函数。也就是说,通过writev函数可以将分散保存在多个缓冲的数据一并发送,通过readv函数可以由多个缓冲分别接收。因此,适当使用这两个函数可以减少I/O函数的调用次数

#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);//成功时返回发送的字节数,失败时返回-1

  

  • filedes:表示数据传输对象的套接字文件描述符,但该函数并不只限于套接字,因此,可以像read函数一样向其传递文件或标准输出描述符
  • iov:iovec结构体数组的地址值,结构体iovec中包含待发送数据的位置和大小信息
  • iovcnt:向第二个参数传递的数组长度

上述函数的第二个参数中出现的数组iovec结构体的声明如下

struct iovec
{
void *iov_base; //缓冲地址
size_t iov_len; //缓冲大小
};

  

可以看到,结构体iovec由保存待发送数据的缓冲(char型数组)地址值和实际发送的数据长度信息构成。给出上述函数的调用示例前,先通过图1-4了解该函数的使用方法

图1-3   write和iovec

图1-3中writev的第一个参数1是文件描述符,因此向控制台输出数据,ptr是存有待发送数据信息的iovec数组指针。第三个参数为2,因此,从ptr指向的地址开始,共浏览两个iovec结构体变量,发送这些指针指向的缓冲数据。接下来仔细观察图中iovec结构体数组,ptr[0](数组第一个元素)的iov_base指向以A开头的字符串,同时iov_len为3,故发送ABC,而ptr[1](数组的第二个元素)的iov_base指向数字1,同时iov_len为4,故发送1234

接下来给出关于writev函数的调用示例

writev.c

#include <stdio.h>
#include <sys/uio.h> int main(int argc, char *argv[])
{
struct iovec vec[2];
char buf1[] = "ABCDEFG";
char buf2[] = "1234567";
int str_len; vec[0].iov_base = buf1;
vec[0].iov_len = 3;
vec[1].iov_base = buf2;
vec[1].iov_len = 4; str_len = writev(1, vec, 2);
puts("");
printf("Write bytes: %d \n", str_len);
return 0;
}

  

  • 第11、12行:写入第一个传输数据的保存位置和大小
  • 第13、14行:写入第二个传输数据的保存位置和大小
  • 第16行:writev函数的第一个参数为1,故向控制台输出数据

编译writev.c并运行

# gcc writev.c -o writev
# ./writev
ABC1234
Write bytes: 7

  

下面介绍readv函数,它与writev函数正好相反

#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);//成功时返回接收的字节数,失败时返回-1

  

  • filedes:传递接收数据的文件(或套接字)描述符
  • iov:包含数据保存位置和大小信息的iovec结构体数组的地址值
  • iovcnt:第二个参数中数组的长度

下面给出readv函数的使用方法

readv.c

#include <stdio.h>
#include <sys/uio.h>
#define BUF_SIZE 100 int main(int argc, char *argv[])
{
struct iovec vec[2];
char buf1[BUF_SIZE] = {0,};
char buf2[BUF_SIZE] = {0,};
int str_len; vec[0].iov_base = buf1;
vec[0].iov_len = 5;
vec[1].iov_base = buf2;
vec[1].iov_len = BUF_SIZE; str_len = readv(0, vec, 2);
printf("Read bytes: %d \n", str_len);
printf("First message: %s \n", buf1);
printf("Second message: %s \n", buf2);
return 0;
}

  

  • 第12、13行:设置第一个数据的保存位置和大小,接收数据的大小已指定为5,因此,无论buf1的大小是多少,最多仅能保存5个字节
  • 第14、15行:vec[0]中注册的缓冲中保存5个字节,剩余数据将保存到vec[1]中注册的缓冲。结构体iovec的成员iov_len中应写入接收的最大字节数
  • 第17行:readv函数的第一个参数为0,因此从标准输入接收数据

编译readv.c并运行

# gcc readv.c -o readv
# ./readv
I like TCP/IP socket programming
Read bytes: 33
First message: I lik
Second message: e TCP/IP socket programming

  

由运行结果可知,通过第7行声明的vec数组保存了数据

合理使用readv和writev函数

哪种情况适合使用readv和writev函数?实际上,能使用该函数的所有情况都适用。例如:需要传输的数据分别位于不同缓冲(数组)时,需要多次调用write函数,此时可以通过一次writev函数调用来提高效率。同样,需要将输入缓冲中的数据读入不同位置时,可以不必多次调用read函数,而是利用一次readv函数就能大大提高效率

即使从C语言角度来看,减少函数调用次数也能相应提高性能。但其更大的意义在于减少数据包个数,假设为了提高效率而在服务端明确禁止了Nagle算法,其实writev函数在不采用Nagle算法时更有价值,如图1-4所示

图1-4   Nagle算法关闭状态下的数据传输

上述示例中待发送的数据分别存在三个不同的地方,此时如果使用write函数则需要三次函数调用。但若为提高速度而关闭了Nagle算法,则极有可能通过三个数据包传递数据。反之,若使用writev函数将所有数据一次性写入输出缓冲,则很有可能仅通过一个数据包传输数据。所以writev函数和readv函数非常有用

再考虑一种情况:将不同位置的数据按照发送顺序移动(复制)到一个大数组,并通过一次write函数调用进行传输。这种方式是否与调用writev函数的效果相同?当然!但使用writev函数更为便利,因此,如果遇到writev函数和readv函数的适用情况,请各位一定要优先考虑writev和readv函数

TCP/IP网络编程之多种I/O函数的更多相关文章

  1. TCP/IP网络编程之套接字的多种可选项

    套接字可选项进而I/O缓冲大小 我们进行套接字编程时往往只关注数据通信,而忽略了套接字具有的不同特性.但是,理解这些特性并根据实际需要进行更改也十分重要.之前我们写的程序在创建好套接字后都是未经特别操 ...

  2. 【TCP/IP网络编程】:09套接字的多种可选项

    本篇文章主要介绍了套接字的几个常用配置选项,包括SO_SNDBUF & SO_RCVBUF.SO_REUSEADDR及TCP_NODELAY等. 套接字可选项和I/O缓冲大小 前文关于套接字的 ...

  3. 《TCP/IP网络编程》

    <TCP/IP网络编程> 基本信息 作者: (韩)尹圣雨 译者: 金国哲 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115358851 上架时间:2014-6- ...

  4. TCP/IP网络编程之多播与广播

    多播 多播方式的数据传输是基于UDP完成的,因此,与UDP服务端/客户端的实现非常接近.区别在于,UDP数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机.换言之,采用多播方式时 ...

  5. TCP/IP网络编程之基于TCP的服务端/客户端(二)

    回声客户端问题 上一章TCP/IP网络编程之基于TCP的服务端/客户端(一)中,我们解释了回声客户端所存在的问题,那么单单是客户端的问题,服务端没有任何问题?是的,服务端没有问题,现在先让我们回顾下服 ...

  6. TCP/IP网络编程之基于TCP的服务端/客户端(一)

    理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流(stream)的套接字.TCP是Transmissi ...

  7. TCP/IP网络编程之套接字类型与协议设置

    套接字与协议 如果相隔很远的两人要进行通话,必须先决定对话方式.如果一方使用电话,另一方也必须使用电话,而不是书信.可以说,电话就是两人对话的协议.协议是对话中使用的通信规则,扩展到计算机领域可整理为 ...

  8. 浅谈TCP/IP网络编程中socket的行为

    我认为,想要熟练掌握Linux下的TCP/IP网络编程,至少有三个层面的知识需要熟悉: 1. TCP/IP协议(如连接的建立和终止.重传和确认.滑动窗口和拥塞控制等等) 2. Socket I/O系统 ...

  9. TCP/IP网络编程系列之四(初级)

    TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的 ...

随机推荐

  1. java8 api简介(一)

    from https://www.aliyun.com/jiaocheng/785076.html 摘要:函数式编程详解:前言:现在有很多公司都用了jdk8,但是函数式编程也许没有用上,jdk8也提供 ...

  2. Laravel事件监听器listener与事件订阅者Subscriber的区别

    其实就一句话: Each event can have multiple listeners, but a listener can't listen to more than a single ev ...

  3. MyBatis源码探索

    每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的.SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获 ...

  4. zabbix-2.2.2(Ubuntu 14.04 LTS/OpenLogic 7.2)

    平台: arm 类型: ARM 模板 软件包: apache-2.4.7 mariadb-5.5.50 mysql-5.5.52-0ubuntu0.14.04.1 php-5.4.16 php-5.5 ...

  5. Android(java)学习笔记84:SQLiteDatabase的query方法参数

    1. SQLiteDatabase的query方法: public Cursor query (boolean distinct, String table, String[] columns, St ...

  6. IBM中国

    https://www.ibm.com/developerworks/cn/linux/l-memory/

  7. hive对有null值的列进行avg,sum,count等操作时会不会过滤null值

    在hive中,我们经常会遇到对某列进行count.sum.avg等操作计算记录数.求和.求平均值等,但这列经常会出现有null值的情况,那这些操作会不会过滤掉null能呢? 下面我们简单测试下: wi ...

  8. 理解dropout

    理解dropout 注意:图片都在github上放着,如果刷不开的话,可以考虑FQ. 转载请注明:http://blog.csdn.net/stdcoutzyx/article/details/490 ...

  9. ERR_FAILED 浏览器访问

    我是针对上一篇的问题 继续的探究  ,我百度了 看了这 https://zhidao.baidu.com/question/1175643597811783659.html 之后我就用 电脑管家进行系 ...

  10. 【6.20校内test】

    反正考的不是很好吧,赶脚炸了啊qwq 然后这两天一直在忙一些神奇的事情,所以没有整理完 手动@water_lift T1:大美江湖: [题目背景] 细雪飘落长街,枫叶红透又一年 不只为故友流连,其实我 ...