当前位置:   article > 正文

5.Linux网络编程-select实现超时API_linux select的超时

linux select的超时

一、alarm函数设置超时
它的主要功能是设置信号传送闹钟。

信号SIGALRM在经过seconds指定的秒数后传送给目前的进程,如果在定时未完成的时间内再次调用了alarm函数,则后一次定时器设置将覆盖前面的设置,当seconds设置为0时,定时器将被取消。
它返回上次定时器剩余时间,如果是第一次设置则返回0。

void sigHandlerForSigAlrm(int signo)  
{  
    return ;  
}  
  
signal(SIGALRM, sigHandlerForSigAlrm);  
alarm(5);  
int ret = read(sockfd, buf, sizeof(buf));  
if (ret == -1 && errno == EINTR)  
{  
    // 阻塞并且达到了5s,超时,设置返回错误码  
    errno = ETIMEDOUT;  
}  
else if (ret >= 0)  
{  
    // 正常返回(没有超时), 则将闹钟关闭  
    alarm(0);  
}  

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

(二)套接字选项: SO_SNDTIMEO, SO_RCVTIMEO,调用setsockopt设置读/写超时时间

//示例: read超时  
int seconds = 5;  
if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &seconds, sizeof(seconds)) == -1)  
    err_exit("setsockopt error");  
int ret = read(sockfd, buf, sizeof(buf));  
if (ret == -1 && errno == EWOULDBLOCK)  
{  
    // 超时,被时钟信号打断  
    errno = ETIMEDOUT;  
}  

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

SO_RCVTIMEO是接收超时,SO_SNDTIMEO是发送超时。这种方式也不经常使用,因为这种方案不可移植,并且有些套接字的实现不支持这种方式。

(三)使用select函数实现超时
select的函数提供了时间参数,可用来控制超时。data.h头文件是实现了相关的超时API。

/*
 * data.h
 *
 *  Created on: 2020年2月21日
 *  
 */

#ifndef SRC_DEMO_DATA_H_
#define SRC_DEMO_DATA_H_

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <fcntl.h>


/**
read_timeout-读超时检测函数,不含读操作
	(即:判断[从fd套接字]中读数据,是否超时,不真正的读走数据)
@fd:文件描述符
@wait_seconds:等待超时秒数,如果为0表示不检测超时
	成功(未超时):返回0
	失败:返回-1
	超时:返回-1并且errno=ETIME_OUT
 */
int read_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {
        fd_set read_fdset;
        struct timeval timeout;

        FD_ZERO(&read_fdset);
        FD_SET(fd, &read_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;

        //select返回值三态
        //1 若timeout时间到(超时),没有检测到读事件 ret返回=0
        //2 若ret返回<0 &&  errno == EINTR 说明select的过程中被别的信号中断(可中断睡眠原理)
        //2-1 若返回-1,select出错
        //3 若ret返回值>0 表示有read事件发生,返回事件发生的个数

        do
        {
            ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);

        if (ret == 0)
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)
            ret = 0;
    }

    return ret;
}


/**
write_timeout-写超时检测函数,不含写操作
	(即:判断[向fd套接字]中写数据,是否超时,不真正的写入数据)
@fd:文件描述符
@wait_seconds:等待超时秒数,如果为0表示不检测超时
	成功(未超时):返回0
	失败:返回-1
	超时:返回-1并且errno=ETIME_OUT
 */
int write_timeout(int fd, unsigned int wait_seconds)
{
    int ret = 0;
    if (wait_seconds > 0)
    {
        fd_set write_fdset;
        struct timeval timeout;

        FD_ZERO(&write_fdset);
        FD_SET(fd, &write_fdset);

        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do
        {
            ret = select(fd + 1, NULL, &write_fdset, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);

        if (ret == 0)
        {
            ret = -1;
            errno = ETIMEDOUT;
        }
        else if (ret == 1)
            ret = 0;
    }

    return ret;
}


/**
 * activate_noblock - 设置I/O为非阻塞模式
 * @fd: 文件描符符
 */
int activate_nonblock(int fd)
{
	int ret = 0;

	int flags = fcntl(fd,F_GETFL);
	if(-1 == flags)
	{
		ret =flags;
		perror("fcntl");
		return ret;
	}

	flags |= O_NONBLOCK;

	ret = fcntl(fd,F_SETFL,flags);
	if(ret == -1)
	{
		perror("fcntl(fd,F_SETFL,flags)");
		return ret;
	}

	return ret;
}

/**
 * deactivate_nonblock - 设置I/O为阻塞模式
 * @fd: 文件描符符
 */
int deactivate_nonblock(int fd)
{
	int ret = 0;

	int flags = fcntl(fd,F_GETFL);
	if(-1 == flags)
	{
		ret =flags;
		perror("fcntl");
		return ret;
	}

	flags &= ~O_NONBLOCK;

	ret = fcntl(fd,F_SETFL,flags);
	if(ret == -1)
	{
		perror("fcntl(fd,F_SETFL,flags)");
		return ret;
	}

	return ret;
	}

/**
connect_timeout
@fd:套接字
@addr:要连接的对方地址
@wait_seconds:等待超时秒数,如果为0表示正常模式
	成功(未超时):返回0
	失败:返回-1
	超时:返回-1并且errno=ETIMEOUT
 */
static int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
	int ret = 0;
	socklen_t addrlen = sizeof(struct sockaddr_in);
	fd_set connect_fdset;
	struct timeval timeout;

	int err;
	socklen_t socklen = sizeof(err);
	int sockoptret;

	if(wait_seconds > 0)//设置成非阻塞--因为文件描述符默认是阻塞的
	{
		activate_nonblock(fd);
	}


	/*
	1.在建立套接字(fd)以后默认是阻塞的(如果客户端连接服务器发生异常,则默认阻塞的情况下,返回时间是1.5RTT,大约100秒!–软件质量低下)
	2.先把套接字通过fcntl变为非阻塞模型,再调用connect函数
	[1]如果网络顺利,直接建立链接
	[2]如果网络不好,则根据返回值判断:如果connect的返回值-1&&errno==EINPROGRESS,则表示客户端和服务器正在建立连接,需要等待一段时间才能建立链接(可以利用select监控该套接字是否可写来设定等待时间),进一步对select返回的结果判断是否可写。
	[3]尽管select返回了套接字的可写状态,但不一定表示就是正确建立链接,(前面已经知道),导致select监控的套接字可读可写有两种情况
	case1:真正的可读可写,即表示建立了连接
	case2:建立套接字产生错误,会返回写失败信息,造成可写入的状态。 此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。


	  非阻塞
	  --成功:立马建立连接
	  --失败:ret < 0 && errno == EINPROGRESS,表示没有获取到链接
	  */
	ret = connect(fd,(struct sockaddr*)addr,addrlen);
	if(ret < 0 && errno == EINPROGRESS)
	{
		FD_ZERO(&connect_fdset);
		FD_SET(fd,&connect_fdset);

		timeout.tv_sec = wait_seconds;
		timeout.tv_usec = 0;

		do
		{
			// 一但连接建立,则套接字就可写  所以connect_fdset放在了写集合中
			ret = select(fd + 1,NULL,&connect_fdset,NULL,&timeout);//在规定时间内监控链接
		}while(ret < 0 && errno == EINTR);

		if(ret == 0)//超时
		{
			ret = -1;
			errno = ETIMEDOUT;
		}
		else if (ret < 0)//select出错
		{
			return -1;
		}
		else if( ret == 1)//有两种情况会导致文件描述符变为可写入的状态/准备好的状态
		{
			/* ret返回为1(表示套接字可写),可能有两种情况,一种是连接建立成功,一种是套接字产生错误,*/
			/* 此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。 */
			sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);//获取套接字的状态
			if(sockoptret == -1)//getsockopt调用失败
			{
				return -1;
			}
			if(err == 0)//若无错误发生,getsockopt()返回0,表示真正可写入/准备好
				ret = 0;
			else
			{//表示套接字产生错误
				errno = err;
				ret = -1;
			}
		}
	}

	if (wait_seconds > 0)
	{
		deactivate_nonblock(fd);
	}

	return ret;
}

/**
 * accept_timeout - 带超时的accept
 * @fd: 套接字
 * @addr: 输出参数,返回对方地址
 * @wait_seconds: 等待超时秒数,如果为0表示正常模式
 * 成功(未超时)返回已连接套接字,超时返回-1并且errno = ETIMEDOUT
 */
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
{
    int ret;

    if (wait_seconds > 0)
    {
        fd_set accept_fdset;
        struct timeval timeout;
        FD_ZERO(&accept_fdset);
        FD_SET(fd, &accept_fdset);
        timeout.tv_sec = wait_seconds;
        timeout.tv_usec = 0;
        do
        {
            ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);
        } while (ret < 0 && errno == EINTR);
        if (ret == -1)
            return -1;
        else if (ret == 0)
        {
            errno = ETIMEDOUT;
            return -1;
        }
    }

    socklen_t addrlen = sizeof(struct sockaddr_in);
    if(addr!=NULL)
    	ret=accept(fd,(struct sockaddr*)addr,&addrlen);
    else
    	ret=accept(fd,NULL,NULL);
    if(ret==-1){
    	perror("accept");
    }

    return ret;
}

#endif /* SRC_DEMO_DATA_H_ */

  • 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
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
//服务器
#include <limits.h> // for OPEN_MAX
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <poll.h>
#include <unistd.h>
#include "data.h"

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)

#define MAXLINE 1024
#define SERV_PORT 5000
#define OPEN_MAX 1024
#define INFTIM -1

//writen函数
//说明:此函数解决了缓冲区数据发送溢出的处理;
//@ssize_t:-1表示返回错误,0表示收到FIN信号,>0表示数据的长度
//@fd:文件描述符
//@buf:待写数据首地址
//@nByte:待写长度
ssize_t writen(int fd, void *buf, size_t nBytes)
{
	size_t nleft = nBytes;
	char *buf_p = (char*)buf;
	int nwritten = 0;

	while(nleft > 0)
	{
		nwritten = write(fd, buf_p, nleft);
		if (nwritten<0 && errno==EINTR)
			nwritten = 0;
		else
			return -1;
		nleft -= nwritten;
		buf_p += nwritten;
	}

	return nBytes;
}


int main(int argc, char **argv)
{
	int i, maxi, listenfd, connfd, sockfd;
	int nready;

	ssize_t n;
	char buf[MAXLINE];
	socklen_t clilen;
	struct pollfd client[OPEN_MAX];
	struct sockaddr_in cliaddr, servaddr;

	//1.创建套接字
	if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
		ERR_EXIT("socket error"); //调用上边的宏

	memset(&servaddr, 0, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);

	//2.设置套接字属性
	int on = 1;
	if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
		ERR_EXIT("setsockopt error");

	//3.绑定
	if (bind(listenfd, (struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
		ERR_EXIT("bind error");

	//4.监听
	if (listen(listenfd, SOMAXCONN) < 0) //listen应在socket和bind之后,而在accept之前
		ERR_EXIT("listen error");

	client[0].fd = listenfd;
	client[0].events = POLLIN;
	for (i = 1; i < OPEN_MAX; ++i)
		client[i].fd = -1;  // indicates available entry
	maxi = 0;  // max index into client[] array
	while (true)
	{
		nready = poll(client, maxi + 1, INFTIM);
		if(nready < 0)
		{
			perror("poll error");
            break;
		}
		else if(nready == 0)
		{
			continue;
		}

		if (client[0].revents & POLLIN)
		{ // new client connection
			clilen = sizeof(cliaddr);
			connfd = accept_timeout(listenfd, &cliaddr, 5);
			if (connfd == -1 && errno == ETIMEDOUT)
			{
				printf("accept timeout\n");
				continue;
			}
			else if (connfd == -1)
			{
				ERR_EXIT("accept error");
			}

			for (i = 1; i < OPEN_MAX; ++i)
			{
				if (client[i].fd < 0)
				{
					client[i].fd = connfd; // save descriptor
					break;
				}
			}
			if (OPEN_MAX == i)
			{
				ERR_EXIT("too many clients");
			}
			client[i].events = POLLIN;
			if (i > maxi)
				maxi = i;  // max index in client[] array

			if (--nready <= 0)
				continue; // no more readable descriptors
		}

		for (i = 1; i <= maxi; ++i)
		{ // check all clients for data
			if ( (sockfd = client[i].fd) < 0)
				continue;
			if (client[i].revents & (POLLIN | POLLERR))
			{
				int ret = read_timeout(sockfd, 5);
				if (ret == 0)
				{
					if ( (n = read(sockfd, buf, MAXLINE)) >= 0)
					{
						if (errno == ECONNRESET)
						{
							// connection reset by client
							close(sockfd);
							client[i].fd = -1;
						}
						else if (0 == n)
						{
							// connection closed by client
							close(sockfd);
							client[i].fd = -1;
						}
						else
						{
							ret = write_timeout(sockfd, 5);
							if (ret == 0)
								writen(sockfd, buf, n);
							else if (ret == -1 && errno == ETIMEDOUT)
							{
								printf("write timeout\n");
								continue;
							}
						}

						if (--nready <= 0)
							break; // no more readable descriptors
					}
					else
					{
						ERR_EXIT("read error");
					}
				}
				else if (ret == -1 && errno == ETIMEDOUT)
				{
					printf("read timeout\n");
					continue;
				}
			}
		}
	}
	return 0;
}

  • 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
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
//客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <poll.h>
#include "data.h"

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)

#define MAXLINE 1024
#define SERV_PORT 5000

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        //  listenfd = socket(AF_INET, SOCK_STREAM, 0)
        ERR_EXIT("socket error");


    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    int ret = connect_timeout(sock, &servaddr, 5);

    if (ret == -1 && errno == ETIMEDOUT)
		ERR_EXIT("connect timeout");
    else if (ret == -1)
        ERR_EXIT("connect error");

    struct sockaddr_in localaddr;
    char cli_ip[20];
    socklen_t local_len = sizeof(localaddr);
    memset(&localaddr, 0, sizeof(localaddr));
    if( getsockname(sock,(struct sockaddr *)&localaddr,&local_len) != 0 )
        ERR_EXIT("getsockname error");
    inet_ntop(AF_INET, &localaddr.sin_addr, cli_ip, sizeof(cli_ip));
    printf("host %s:%d\n", cli_ip, ntohs(localaddr.sin_port));

    struct pollfd p_fds[2];
    int fd_stdin = fileno(stdin);
	p_fds[0].fd = fd_stdin;
	p_fds[0].events = POLLIN;
	p_fds[0].revents = 0;
	p_fds[1].fd = sock;
	p_fds[1].events = POLLIN;
	p_fds[1].revents = 0;

    int nready;
    int maxfd;
    if (fd_stdin > sock)
        maxfd = fd_stdin;
    else
        maxfd = sock;
    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};

    while (true)
    {
		nready = poll(p_fds, 2, -1);
        if(nready < 0)
            ERR_EXIT("poll");
        else if(nready == 0)
        {
            printf("timeout\n");
            continue;
        }
        else
        {
        	if (p_fds[1].revents & POLLIN)
			{
				int ret = read(sock, recvbuf, sizeof(recvbuf));
				if (ret == -1)
					ERR_EXIT("read error");
				else if (ret  == 0 || errno == ECONNRESET)   //服务器关闭
				{
					p_fds[1].fd = -1;
					break;
				}

				fputs(recvbuf, stdout);
				memset(recvbuf, 0, sizeof(recvbuf));
			}

			if (p_fds[0].revents & POLLIN)
			{
				if (fgets(sendbuf, sizeof(sendbuf), stdin) == NULL)
					break;
				write(sock, sendbuf, strlen(sendbuf));
				memset(sendbuf, 0, sizeof(sendbuf));
			}
        }
    }

    close(sock);
    return 0;
}


  • 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
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/225151
推荐阅读
相关标签
  

闽ICP备14008679号