简介:

Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器.

  • 使用线程池 + epoll(ET和LT均实现) + 模拟Proactor模式的并发模型
  • 使用状态机解析HTTP请求报文,支持解析GET和POST请求
  • 通过访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件
  • 实现同步/异步日志系统,记录服务器运行状态
  • 经Webbench压力测试可以实现上万的并发连接数据交换

基础测试

  • 服务器测试环境

    • Ubuntu版本16.04
    • MySQL版本5.7.29
  • 浏览器测试环境

    • Windows、Linux均可
    • Chrome
    • FireFox
    • 其他浏览器暂无测试
  • 测试前确认已安装MySQL数据库

    // 建立yourdb库
    create database yourdb;// 创建user表
    USE yourdb;
    CREATE TABLE user(username char(50) NULL,passwd char(50) NULL
    )ENGINE=InnoDB;// 添加数据
    INSERT INTO user(username, passwd) VALUES('name', 'passwd');
    
  • 修改main.c中的数据库初始化信息

    // root root修改为服务器数据库的登录名和密码
    // qgydb修改为上述创建的yourdb库名
    connPool->init("localhost", "root", "root", "yourdb", 3306, 8);
    
  • 修改http_conn.cpp中的root路径

    // 修改为root文件夹所在路径
    const char* doc_root="/home/qgy/TinyWebServer/root";
    
  • 生成server

    make server
    
  • 启动server

    ./server port
    
  • 浏览器端

    ip:port
    

个性化测试

  • I/O复用方式,listenfd和connfd可以使用不同的触发模式,代码中使用LT + LT模式,可以自由修改与搭配.
  • LT + LT模式

    • listenfd触发模式,关闭main.c中listenfdET,打开listenfdLT

      26 //#define listenfdET       //边缘触发非阻塞
      27 #define listenfdLT         //水平触发阻塞
      
    • listenfd触发模式,关闭http_conn.cpp中listenfdET,打开listenfdLT

      10 //#define listenfdET       //边缘触发非阻塞
      11 #define listenfdLT         //水平触发阻塞
      
    • connfd触发模式,关闭http_conn.cpp中connfdET,打开connfdLT

      7 //#define connfdET       //边缘触发非阻塞
      8 #define connfdLT         //水平触发阻塞
      
  • LT + ET模式

    • listenfd触发模式,关闭main.c中listenfdET,打开listenfdLT

      26 //#define listenfdET       //边缘触发非阻塞
      27 #define listenfdLT         //水平触发阻塞
      
    • listenfd触发模式,关闭http_conn.cpp中listenfdET,打开listenfdLT

      10 //#define listenfdET       //边缘触发非阻塞
      11 #define listenfdLT         //水平触发阻塞
      
    • connfd触发模式,打开http_conn.cpp中connfdET,关闭connfdLT

      7 #define connfdET       //边缘触发非阻塞
      8 //#define connfdLT         //水平触发阻塞
      
  • 日志写入方式,代码中使用同步日志,可以修改为异步写入.
  • 同步写入日志

    • 关闭main.c中ASYNLOG,打开同步写入SYNLOG

      25 #define SYNLOG //同步写日志
      26 //#define ASYNLOG   /异步写日志
      
  • 异步写入日志

    • 关闭main.c中SYNLOG,打开异步写入ASYNLOG

      25 //#define SYNLOG //同步写日志
      26 #define ASYNLOG   /异步写日志
      
  • 选择I/O复用方式或日志写入方式后,按照前述生成server,启动server,即可进行测试.

原文链接

二、编译搭建

        1.将完整的代码编译gcc tinyserver.c -o tinyserver2.将测试程序adder.c编译成可执行程序,adder.c需放在与tinyserver在同一目录下的cgi-bin文件夹下(后面再说为什么这样放)gcc adder.c -o adder3.运行tinyserver程序并指定所用端口(1024--49151可用,其他为系统使用,一般不能占用)./tinyserver 20004.在浏览器中地址栏输入访问地址http:localhost:2000/cgi-bin/adder?30&725.运行结果浏览器中显示:后台服务器信息显示:

如果想要显示其他的文件,例如图片,文章等做法和上面一样

    http:localhost:2000/testpic.jpg

但是如果在测试的过程中会遇到下面的情况,后台显示一直在刷新

我想可能是因为这个服务器是单线程的原因,当接收到一个请求后,在main中由于是持续刷新的,才会出现这种情况,但是我不是很确定,不知道哪位大神可以解释下.........

以上就是简单的使用情况,相信在测试成功的那一刻,是不是成就感很大有没有啊???

三、源码分析

Tiny是一个迭代服务器,监听在命令行中确定的端口上的连接请求。在通过open_listenedfd函数打开一个监听套接字以后,Tiny执行典型的无限服务循环,反复地接受一个连接(accept)请求,执行事务(doit),最后关闭连接描述符(close)

1.头文件:

/*TINY - A simple ,iterative HTTP/1.0 Web server
*/
#ifndef __CSAPP_H__
#define __CSAPP_H__
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <setjmp.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <math.h>
#include <semaphore.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//以上的头文件按说都是在”csapp.h”中,但是我试了试不行的,所以就直接自己写了
#define DEF_MODE   S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
#define DEF_UMASK  S_IWGRP|S_IWOTH
typedef struct sockaddr SA;
#define RIO_BUFSIZE 8192
typedef struct {  int rio_fd;                /* 内部缓存区的描述符 */  int rio_cnt;               /* 内部缓存区剩下还未读的字节数 */  char *rio_bufptr;          /* 指向内部缓存区中下一个未读字节 */  char rio_buf[RIO_BUFSIZE]; /* 内部缓存区 */
} rio_t;
extern char **environ;
#define MAXLINE  8192  /* 每行最大字符数 */
#define MAXBUF   8192  /* I/O缓存区的最大容量 */
#define LISTENQ  1024  /* 监听的第二个参数 */
/* helper functions */
ssize_t rio_writen(int fd,void *usrbuf,size_t n);
void rio_readinitb(rio_t *rp,int fd);  //将程序的内部缓存区与描述符相关联。
ssize_t rio_readlineb(rio_t *rp,void *usrbuf,size_t maxlen);  /*从内部缓存区读出一个文本行至buf中,以null字符来结束这个文本行。当然,每行最大的字符数量不能超过MAXLINE。*/
int open_clientfd(char *hostname, int portno);
int open_listenfd(int portno);
#endif   void doit(int fd);
void read_requesthdrs(rio_t *rp);  //读并忽略请求报头
int parse_uri(char *uri, char *filename, char *cgiargs);   //解析uri,得文件名存入filename中,参数存入cgiargs中。
void serve_static(int fd, char *filename, int filesize);   //提供静态服务。
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *cause, char *cgiargs);    //提供动态服务。
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);
/*Tiny是一个迭代服务器,监听在命令行中确定的端口上的连接请求。在通过open_listenedfd函数打开一个监听套接字以后,Tiny执行典型的无限服务循环,反复地接受一个连接(accept)请求,执行事务(doit),最后关闭连接描述符(close)
*/
/*sscanf(buf,"%s %s %s",method,uri,version) :作为例子,一般此时buf中存放的是“GET / HTTP/1.1”,所以可知method为“GET”,uri为“/”,version为“HTTP/1.1”。其中sscanf的功能:把buf中的字符串以空格为分隔符分别传送到method、uri及version中。strcasecmp(method,"GET") :忽略大小写比较method与“GET”的大小,相等的话返回0。stat(filename,&sbuf) :将文件filename中的各个元数据填写进sbuf中,如果找不到文件返回0。S_ISREG(sbuf,st_mode) :此文件为普通文件。S_IRUSR & sbuf.st_mode :有读取权限。
*/

2.Tiny的main函数

int main(int argc, char const *argv[])
{int listenfd, connfd, port, clientlen;struct sockaddr_in clientaddr;if(argc != 2) {fprintf(stderr, "usage: %s\n", argv[0]);exit(1);}    port = atoi(argv[1]);listenfd = open_listenfd(port);while(1) {clientlen = sizeof(clientaddr);connfd = accept(listenfd,(SA *)&clientaddr,&clientlen);doit(connfd);close(connfd);}
}

3.Tiny的doit函数

void doit(int fd)
{int is_static;struct stat sbuf;char buf[MAXLINE],method[MAXLINE],uri[MAXLINE],version[MAXLINE];char filename[MAXLINE],cgiargs[MAXLINE];rio_t rio;rio_readinitb(&rio,fd);rio_readlineb(&rio,buf,MAXLINE);sscanf(buf,"%s %s %s",method,uri,version);if(strcasecmp(method,"GET")) {clienterror(fd,method,"501","Not Implemented","Tiny does not implement this method");return;}read_requesthdrs(&rio);is_static = parse_uri(uri,filename,cgiargs);if(stat(filename,&sbuf) < 0) {clienterror(fd,filename, "404", "Not found","Tiny coundn't find this file");return;}if(is_static) {if(!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {clienterror(fd,filename, "403", "Forbidden","Tiny coundn't read the file");return;}serve_static(fd,filename,sbuf.st_size);}else {if(!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {clienterror(fd,filename, "403", "Forbidden","Tiny coundn't run the CGI program");return;}serve_dynamic(fd,filename,cgiargs);}
}
/*
从doit函数中可知,我们的Tiny Web服务器只支持“GET”方法,其他方法请求的话则会发送一条错误消息,主程序返回
,并等待下一个请求。否则,我们读并忽略请求报头。(其实,我们在请求服务时,直接不用写请求报头即可,写上只是
为了符合HTTP协议标准)。
然后,我们将uri解析为一个文件名和一个可能为空的CGI参数,并且设置一个标志位,表明请求的是静态内容还是动态
内容。通过stat函数判断文件是否存在。
最后,如果请求的是静态内容,我们需要检验它是否是一个普通文件,并且可读。条件通过,则我们服务器向客服端发送
静态内容;相似的,如果请求的是动态内容,我就核实该文件是否是可执行文件,如果是则执行该文件,并提供动态功能。

*/

4.Tiny的clienterrorh函数

void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg)
{char buf[MAXLINE],body[MAXBUF];sprintf(body,"<html><title>Tiny Error</title>");sprintf(body,"%s<body bgcolor=""ffffff"">\r\n",body);sprintf(body,"%s%s: %s\r\n",body,errnum,shortmsg);sprintf(body,"%s<p>%s: %s\r\n",body,longmsg,cause);sprintf(body,"%s<hr><em>The Web server</em>\r\n",body);sprintf(buf,"HTTP/1.0 %s %s\r\n",errnum,longmsg);rio_writen(fd,buf,strlen(buf));sprintf(buf,"Content-type: text/html\r\n");rio_writen(fd,buf,strlen(buf));sprintf(buf,"sContent-length: %d\r\n\r\n",(int)strlen(body));rio_writen(fd,buf,strlen(buf));rio_writen(fd,body,strlen(body));
}
/*
向客户端返回错误信息。
sprintf(buf,"------------"):将字符串“------------”输送到buf中。
rio_writen(fd,buf,strlen(buf)):将buf中的字符串写入fd描述符中。
*/
## 5.Tiny的
void read_requesthdrs(rio_t *rp)
{char buf[MAXLINE];rio_readlineb(rp,buf,MAXLINE);while(strcmp(buf,"\r\n")) {rio_readlineb(rp,buf,MAXLINE);printf("%s", buf);}return;
}
/*Tiny不需要请求报头中的任何信息,这个函数就是来跳过这些请求报头的,读这些请求报头,直到空行,然后返回。
*/

6.Tiny的

int parse_uri(char *uri, char *filename,char *cgiargs)
{
char *ptr;

if(!strstr(uri,"cgi-bin")) {strcpy(cgiargs,"");strcpy(filename,".");strcat(filename,uri);if(uri[strlen(uri)-1] == '/') {strcat(filename,"home.html");}return 1;
}
else {ptr = index(uri,'?');if(ptr) {strcpy(cgiargs,ptr+1);*ptr = '\0';}else {strcpy(cgiargs,"");}strcpy(filename,".");strcat(filename,uri);return 0;
}

}
/*
根据uri中是否含有cgi-bin来判断请求的是静态内容还是动态内容。如果没有cgi-bin,则说明请求的是静态内容。那么
,我们需把cgiargs置NULL,然后获得文件名,如果我们请求的uri最后为 “/”,则自动添加上home.html。比如说,我
请求的是“/”,则返回的文件名为“./home.html”,而我们请求“/logo.gif”,则返回的文件名为“./logo.gif”。如果
uri中含有cgi-bin,则说明请求的是动态内容。那么,我们需要把参数拷贝到cgiargs中,把要执行的文件路径写入
ilename。举例来说,uri为/cgi-bin/adder?12&45,则cigargs中存放的是12&45,filename中存放的是
“./cgi-bin/adder”
index(uri,’?’) : 找出uri字符串中第一个出现参数‘?’的地址,并将此地址返回。
*/

7.Tiny的serve_static函数

void serve_static(int fd, char *filename, int filesize)
{int srcfd;char *srcp,filetype[MAXLINE],buf[MAXBUF];get_filetype(filename,filetype);sprintf(buf,"HTTP/1.0 200 OK\r\n");sprintf(buf,"%sServer:Tiny Web Server\r\n",buf);sprintf(buf,"%sContent-length:%d\r\n",buf,filesize);sprintf(buf,"%sContent-type:%s\r\n\r\n",buf,filetype);rio_writen(fd,buf,strlen(buf));srcfd = open(filename,O_RDONLY,0);srcp = mmap(0,filesize, PROT_READ, MAP_PRIVATE,srcfd,0);close(srcfd);rio_writen(fd,srcp,filesize);munmap(srcp,filesize);
}
/*打开文件名为filename的文件,把它映射到一个虚拟存储器空间,将文件的前filesize字节映射到从地址srcp开始的虚拟存储区域。关闭文件描述符srcfd,把虚拟存储区的数据写入fd描述符,最后释放虚拟存储器区域。
*/
void get_filetype(char *filename, char *filetype)
{if(strstr(filename,".html"))strcpy(filetype,"text/html");else if(strstr(filename,".gif"))strcpy(filetype,"image/gif");else if(strstr(filename,".jpg"))strcpy(filetype,"image/jpg");else strcpy(filetype,"text/plain");
}

8.Tiny的server_dynamic函数

void serve_dynamic(int fd, char *filename, char *cgiargs)
{char buf[MAXLINE],*emptylist[] = {NULL};sprintf(buf,"HTTP/1.0 200 OK\r\n");rio_writen(fd,buf,strlen(buf));sprintf(buf,"Server:Tiny Web Server\r\n");rio_writen(fd,buf,strlen(buf));if(fork() == 0) {setenv("QUERY_STRING",cgiargs,1);dup2(fd,STDOUT_FILENO);execve(filename,emptylist,environ);}wait(NULL);
}
/*Tiny通过派生一个子进程并在子进程的上下文中运行一个cgi程序(可执行文件),来提供各种类型的动态内容。setenv("QUERY_STRING",cgiargs,1) :设置QUERY_STRING环境变量。dup2 (fd,STDOUT_FILENO) :重定向它的标准输出到已连接描述符。此时,任何写到标准输出的东西都直接写到客户端。execve(filename,emptylist,environ) :加载运行cgi程序。
*/

9.一些其他的函数

ssize_t rio_writen(int fd, void *usrbuf, size_t n)
{size_t nleft = n;ssize_t nwritten;char *bufp = usrbuf;while (nleft > 0) {if ((nwritten = write(fd, bufp, nleft)) <= 0) {if (errno == EINTR)  nwritten = 0;   elsereturn -1;      }nleft -= nwritten;bufp += nwritten;}return n;
}
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
{int cnt;while (rp->rio_cnt <= 0) {  /* 如果缓存区空,则重新填充 */rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf));if (rp->rio_cnt < 0) {if (errno != EINTR) return -1;}else if (rp->rio_cnt == 0)  /* EOF */return 0;else rp->rio_bufptr = rp->rio_buf; /* 重新设置缓存区指针 */}/* 从内部缓存区拷贝 min(n, rp->rio_cnt) 个字节到usrbuf*/cnt = n;          if (rp->rio_cnt < n)   cnt = rp->rio_cnt;memcpy(usrbuf, rp->rio_bufptr, cnt);rp->rio_bufptr += cnt;rp->rio_cnt -= cnt;return cnt;
}
void rio_readinitb(rio_t *rp, int fd)
{rp->rio_fd = fd;  rp->rio_cnt = 0;  rp->rio_bufptr = rp->rio_buf;
}
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
{int n, rc;char c, *bufp = usrbuf;for (n = 1; n < maxlen; n++) { if ((rc = rio_read(rp, &c, 1)) == 1) {*bufp++ = c;if (c == '\n')break;}else if (rc == 0) {if (n == 1)return 0; /* EOF, no data read */elsebreak;    /* EOF, some data was read */} elsereturn -1;   /* error */}*bufp = 0;return n;
}
int open_clientfd(char *hostname, int port)
{int clientfd;struct hostent *hp;struct sockaddr_in serveraddr;if ((clientfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)return -1; if ((hp = gethostbyname(hostname)) == NULL)return -2; bzero((char *) &serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;bcopy((char *)hp->h_addr_list[0], (char *)&serveraddr.sin_addr.s_addr, hp->h_length);serveraddr.sin_port = htons(port);if (connect(clientfd, (SA *) &serveraddr, sizeof(serveraddr)) < 0)return -1;return clientfd;
}
int open_listenfd(int port)
{int listenfd, optval=1;struct sockaddr_in serveraddr;/* 创建一个套接字描述符 */if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)return -1;/* Eliminates "Address already in use" error from bind. */if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int)) < 0)return -1;/* Listenfd will be an endpoint for all requests to porton any IP address for this host */bzero((char *) &serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons((unsigned short)port); if (bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr)) < 0)return -1;/* Make it a listening socket ready to accept connection requests */if (listen(listenfd, LISTENQ) < 0)return -1;return listenfd;
}

tinyserver小型服务器相关推荐

  1. 小型项目服务器要多少,小型服务器需要什么配置

    小型服务器需要什么配置 内容精选 换一换 实例即云耀云服务器,是由CPU.内存.操作系统.云硬盘组成的基础的计算组件.云耀云服务器创建成功后,您就可以像使用自己的本地PC或物理服务器一样,在云上使用云 ...

  2. 万人在线机房服务器配置,如何搭建小型服务器机房?

    有许多企业想要搭建自己的专用服务器从而进行数据的存储,但服务器机房的问题一直困扰着企业,不仅是资金,更是缺乏搭建机房所需要的基础知识. 如何搭建小型服务器机房? 这里我们简单介绍下搭建机房需要考虑哪些 ...

  3. 华为悦盒EC6108V9C变小型服务器和NAS

    华为悦盒EC6108V9C刷成小型服务器和NAS 机顶盒参数 架构:arm7 系统:ubuntu 20.04.5 32位 CPU:hi3798mv100 内存:1G 存储:8G 重点!!! 刷机视频 ...

  4. Android 端的基于TCP的小型服务器_超级简单

    服务端代码: HttpServer: package example.com.httpserver;import android.graphics.Bitmap; import android.gra ...

  5. 如何建一个小型服务器存文件,小型文件服务器

    小型文件服务器 内容精选 换一换 本节操作介绍本地Linux操作系统主机通过SCP向Linux云服务器传输文件的操作步骤.登录管理控制台,在ECS列表页面记录待上传文件的云服务器的弹性公网IP.上传文 ...

  6. 小型服务器的操作系统,小型机服务器的操作系统

    小型机服务器的操作系统 内容精选 换一换 源端服务器为IBM或HP小型机,操作系统不在华为云支持的操作系统列表内,数据库为任意数据库,迁移到华为云后操作系统改变为华为云支持的系统,数据库保持不变.此场 ...

  7. 有一台服务器远程失败其他电脑可以_使用闲置电视盒子打造家庭网盘和远程下载器和小型服务器(二)...

    使用闲置电视盒子打造家庭网盘和远程下载器系列(二) 本系列分为五章 一.综述 二.电视盒子的root 三.app的安装和服务器环境的搭建 四.网盘系统的部署和使用 五.远程下载系统搭建和实现 昨天写了 ...

  8. 使用Netty实现一个小型服务器(作为数据中转站)

    项目需要,要写一个服务器作为数据中转站,服务器接收客户端发送的数据,再转发给其他客户端. 客户端可以自己写一个测试是否可以连接和接收数据,也可以是4G模块或者单片机啥的. 客户端代码: 我的项目目录结 ...

  9. 斐讯db2_斐讯P1刷armbian变身linux小型服务器

    前情提要 斐讯P1即之前的N1天天链矿机,N1翻车后不到一年的时间内被玩机圈硬生生搞成了SBC(单板电脑),玩出了电视盒子.Linux服务器.NAS.路由器等多种花样,连官方也受到启发把N1改头换面成 ...

  10. http小型服务器搭建

    临时搭建一个http的服务器,来访问.有一个叫HFS的软件,可以用 http://www.rejetto.com/hfs/

最新文章

  1. c 语言 按位与或非运算符,C++中的按位与、按位与或|、按位异或^运算符详解
  2. python 如何查看模块所有方法-Python查看模块函数,查看函数方法的详细信息
  3. 面试了100个运营,发现具备这些思维的人才能走的更远
  4. 一个很不错的LINUX基本操作归纳
  5. php javascript 不执行,javascript – 不工作php按钮来执行操作
  6. leetcode —— 17. 电话号码的字母组合
  7. windowsCE镜像文件结构
  8. mongodb的文档游标
  9. IOS上复制粘贴号码到input有空格及input位数限制问题详解
  10. python中颜色空间直方图_OpenCV—python 颜色空间(RGB,HSV,Lab)与 颜色直方图
  11. 可以判断用户打开页面次数吗?_看前端如何单枪匹马实现小程序页面级版本控制...
  12. 零压力入门算法的顶流畅销书《漫画算法》施展了哪些“魔法”?
  13. 小D课堂 - 零基础入门SpringBoot2.X到实战_第4节 Springboot2.0单元测试进阶实战和自定义异常处理_18、SpringBoot测试进阶高级篇之MockMvc讲解...
  14. Spring MVC-学习笔记(4)数据绑定流程
  15. paip.python php的未来预测以及它们的比较优缺点
  16. EXCEL VBA编程入门四:录制宏
  17. 《最受欢迎的男友职业排行榜Top10》
  18. Excel如何批量生成二维码
  19. android 单独设置APP语言
  20. Android动态生成答题卡,手机扫描答题卡改卷的最佳选择——ZipGrade

热门文章

  1. 如何查看已删除的微信聊天记录?教你两招,找到答案
  2. 什么是自然语言处理(NLP)?
  3. 解决spacedesk卸载/重装软件时显示 指定的账户已存在
  4. 2021年机修钳工(中级)考试资料及机修钳工(中级)新版试题
  5. 高速钢(HSS)金属切削刀具的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
  6. python数据分析——简单且有用的代码
  7. 计算机技术学硕国家线,关于工科国家线专硕学硕
  8. 计算机专业外出交流方案,公开学院计算机系外出考察方案.doc
  9. STM32F03学习笔记之ADC配置(含DMA配置)
  10. Kotlin For循环详解