linux下libevent的安装和使用例子:数据回显
来源:互联网 发布:算法基础与在线实践 编辑:程序博客网 时间:2024/06/17 03:02
1、背景介绍
轻量级,开源高性能网络库。跨平台,支持Windows、Linux、*BSD和Mac Os;
1)支持用户三种类型的事件(事件驱动(event-driven)):支持网络I/O,定时器和信号等事件。定时器的数据结构使用最小堆(Min Heap),以提高效率。网络IO和信号的数据结构采用了双向链表(TAILQ)。在实现上主要有3种链表:EVLIST_INSERTED, EVLIST_ACTIVE, EVLIST_TIMEOUT,一个ev在这3种链表之间被插入或删除,处于EVLIST_ACTIVE链表中的ev最后将会被调度执行。
2)支持多种I/O多路复用技术, epoll、poll、dev/poll、select和kqueue等;
3)注册事件优先级
2、安装
tar zxvf libevent-2.0.22-stable.tar.gz
cd libevent-2.0.22-stable./configure -prefix=/usr/libevent
make
sudo make install
安装之后,再重启下。
也可以采用以下:
进行安装。apt-cache search libevent和
apt-get install libevent-dev
3、linux下用qtcreator进行编程的时候注意点
在安装之后,利用Qtcreator进行项目管理的时候,需要在pro文件中添加如下:
正如在gcc中编译的时候,添加如下:
gcc -o basic basic.c -levent。
否则会出现未定义的情况。
4、使用例子(服务器回显):
(1)创建socket,bind,listen,设置为非阻塞模式
//创建一个event_basestruct event_base *base = event_base_new();assert(base != NULL);struct event_base *base = event_base_new()用以创建一个事件处理的全局变量,可以理解为这是一个负责集中处理各种出入IO事件的总管家,它负责接收和派发所有输入输出IO事件的信息,这里调用的是函数event_base_new(), 很多程序里这里用的是event_init(),区别就是前者是线程安全的、而后者是非线程安全的,后者在其官方说明中已经被标志为过时的函数、且建议用前者代替,libevent中还有很多类似的函数,比如建议用event_base_dispatch代替event_dispatch,用event_assign代替event_set和event_base_set等,关于libevent接口的详细说明见其官方说明libevent_doc。
event_base内部有一个循环,循环阻塞在epoll/kqueue等系统调用上,直到有一个或者一些事件发生,然后去处理这些事件。当然,这些事件要被绑定在这个event_base上。每个事件对应一个struct event,可以是监听一个fd或者POSIX信号量之类。struct event使用event_new来创建和绑定,使用event_add来启用:
(3)创建一个event对象,并且将其监听的socket托管给event_base,指定要监听的事件类型,并绑上相应的回调函数
//创建并绑定一个eventstruct event *listen_event;//参数:event_base, 监听的fd,事件类型及属性,绑定的回调函数,给回调函数的参数listen_event = event_new(base, listener, EV_READ|EV_PERSIST, callback_func, (void*)base);
(4)通过event_add方法启动监听事件
//参数:event,超时时间(struct timeval *类型的,NULL表示无超时设置)event_add(listen_event, NULL);
(5)进入事件循环
//启动事件循环event_base_dispatch(base);
接下来关注下绑定到event的回调函数callback_func:传递给它的是一个socket fd、一个event类型及属性bit_field、以及传递给event_new的最后一个参数(去上面几行回顾一下,把event_base给传进来了,实际上更多地是分配一个结构体,把相关的数据都撂进去,然后丢给event_new,在这里就能取得到了)。其原型是:
typedef void(* event_callback_fn)(evutil_socket_t sockfd, short event_type, void *arg)
小结下:
对于一个服务器而言,上面的流程大概是这样组合的:
a. listener = socket(),bind(),listen(),设置nonblocking(POSIX系统中可使用fcntl设置,windows不需要设置,实际上libevent提供了统一的包装evutil_make_socket_nonblocking)
b. 创建一个event_base
c. 创建一个event,将该socket托管给event_base,指定要监听的事件类型,并绑定上相应的回调函数(及需要给它的参数)。对于listener socket来说,只需要监听EV_READ|EV_PERSIST
d. 启用该事件
e. 进入事件循环
f. (异步) 当有client发起请求的时候,调用该回调函数,进行处理。
整理后的完整代码如下:
#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <assert.h>#include <string.h>#include <event.h>#include <sys/types.h>#include <unistd.h>#include <netinet/in.h>#include <sys/socket.h>#include <event2/event.h>#include <event2/bufferevent.h>#define PORT 25341#define BACKLOG 5#define MEM_SIZE 1024struct event_base* base;struct sock_ev { struct event* read_ev; struct event* write_ev; char* buffer;};void release_sock_event(struct sock_ev* ev){ event_del(ev->read_ev); free(ev->read_ev); free(ev->write_ev); free(ev->buffer); free(ev);}void on_accept(int sock, short event, void* arg);void on_read(int sock, short event, void* arg);void on_write(int sock, short event, void* arg);int main(int argc, char* argv[]){ struct sockaddr_in my_addr; int sock; //创建套接字描述符,实质是一个文件描述符 //AF_INET表示使用IP地址,SOCK_STREAM表示使用流式套接字 sock = socket(AF_INET, SOCK_STREAM, 0); int yes = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)); memset(&my_addr, 0, sizeof(my_addr)); //实例化对象的属性 my_addr.sin_family = AF_INET; my_addr.sin_port = htons(PORT); my_addr.sin_addr.s_addr = INADDR_ANY; //将套接字地址和套接字描述符绑定起来 bind(sock, (struct sockaddr*)&my_addr, sizeof(struct sockaddr)); //监听该套接字,连接的客户端数量最多为BACKLOG listen(sock, BACKLOG); //声明事件 struct event listen_ev; //创建基事件 base = event_base_new(); //设置回调函数.将event对象监听的socket托管给event_base,指定要监听的事件类型,并绑上相应的回调函数 event_set(&listen_ev, sock, EV_READ|EV_PERSIST, on_accept, NULL);// //上述操作说明在listen_ev这个事件监听sock这个描述字的读操作(EV_READ),当读消息到达则调用on_accept函数,EV_PERSIST参数告诉系统持续的监听sock上的读事件, //不指定这个属性的话,回调函数被触发后,事件会被删除.所以,如果不加该参数,每次要监听该事件时就要重复的调用event_add函数,从前面的代码可知, //sock这个描述字是bind到本地的socket端口上,因此其对应的可读事件自然就是来自客户端的连接到达,我们就可以调用accept无阻塞的返回客户的连接了。 //使从属于基事件.将listen_ev注册到base这个事件中,相当于告诉处理IO的管家请留意我的listen_ev上的事件。 event_base_set(base, &listen_ev); //有时候看到使用<span style="color:#FF0000;">event_new</span>(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base)代替event_set和event_base_set这两个函数 //添加到事件队列当中.相当于告诉处理IO的管家,当有我的事件到达时你发给我(调用on_accept函数),至此对listen_ev的初始化完毕 event_add(&listen_ev, NULL); //开始循环.正式启动libevent的事件处理机制,使系统运行起来.event_base_dispatch是一个无限循环 event_base_dispatch(base); return 0;}void on_accept(int sock, short event, void* arg){ struct sockaddr_in cli_addr; int newfd; socklen_t sin_size; // read_ev must allocate from heap memory, otherwise the program would crash from segmant fault struct event* read_ev = (struct event*)malloc(sizeof(struct event)); sin_size = sizeof(struct sockaddr_in); newfd = accept(sock, (struct sockaddr*)&cli_addr, &sin_size);//指定服务端去接受客户端的连接 //客户的描述字newfd上监听可读事件,当有数据到达是调用on_read函数 event_set(read_ev, newfd, EV_READ|EV_PERSIST, on_read, read_ev); event_base_set(base, read_ev); event_add(read_ev, NULL); //这里需要注意两点,一是read_ev需要从堆里malloc出来,如果是在栈上分配,那么当函数返回时变量占用的内存会被释放, //因此事件主循环event_base_dispatch会访问无效的内存而导致进程崩溃(即crash);第二个要注意的是event_set中,read_ev作为参数传递给了on_read函数}void on_read(int sock, short event, void* arg){ struct event* write_ev; int size; char* buffer = (char*)malloc(MEM_SIZE); bzero(buffer, MEM_SIZE); size = recv(sock, buffer, MEM_SIZE, 0); printf("receive data:%s, size:%d\n", buffer, size); if (size == 0)//当从socket读返回0标志,对方已经关闭了连接,因此这个时候就没必要继续监听该套接口上的事件 { event_del((struct event*)arg); //由于EV_READ在on_accept函数里是用EV_PERSIST参数注册的,因此要显示的调用event_del函数取消对该事件的监听 free((struct event*)arg); close(sock); return; } write_ev = (struct event*) malloc(sizeof(struct event)); event_set(write_ev, sock, EV_WRITE, on_write, buffer);//写时调用on_write函数,注意将buffer作为参数传递给了on_write event_base_set(base, write_ev); event_add(write_ev, NULL);}// on_write函数中向客户端回写数据,然后释放on_read函数中malloc出来的buffer。在很多书合编程指导中都很强调资源的所有权,经常要求谁分配资源、就由谁释放资源,//这样对资源的管理指责就更明确,不容易出问题,但是通过该例子我们发现在异步编程中资源的分配与释放往往是由不同的所有者操作的,因此也是比较容易出问题的地方。void on_write(int sock, short event, void* arg){ char* buffer = (char*)arg; send(sock, buffer, strlen(buffer), 0); free(buffer);}
再来看看前面提到的on_read函数中存在的问题,首先write_ev是动态分配的内存,但是没有释放,因此存在内存泄漏,另外,on_read中进行malloc操作,那么当多次调用该函数的时候就会造成内存的多次泄漏。这里的解决方法是对socket的描述字可以封装一个结构体来保护读、写的事件以及数据缓冲区,
2)Libevent buffer实现异步传输
在Linux下有epoll,BSDS有kqueue,Solaris有evport和/dev/poll等等可以实现异步传输,但是没有哪一个操作系统拥有他们全部,而libevent就是把这些接口都封装起来,并且无论哪一个系统使用它都是最高效的。
bufferevent是个神器。struct bufferevent内建了两个event(read/write)和对应的缓冲区【struct evbuffer *input, *output】,并提供相应的函数用来操作缓冲区(或者直接操作bufferevent)。每当有数据被读入input的时候,read_cb函数被调用;每当output被输出完的时候,write_cb被调用;在网络IO操作出现错误的情况(连接中断、超时、其他错误),error_cb被调用。于是上一部分的步骤被简化为:
1. 设置sockfd为nonblocking
2. 使用bufferevent_socket_new创建一个struct bufferevent *bev,关联该sockfd,托管给event_base
3. 使用bufferevent_setcb(bev, read_cb, write_cb, error_cb, (void *)arg)将EV_READ/EV_WRITE对应的函数
4. 使用bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST)来启用read/write事件
------
5. (异步)
在read_cb里面从input读取数据,处理完毕后塞到output里(会被自动写入到sockfd)
在write_cb里面(需要做什么吗?对于一个echo server来说,read_cb就足够了)
在error_cb里面处理遇到的错误
*. 可以使用bufferevent_set_timeouts(bev, struct timeval *READ, struct timeval *WRITE)来设置读写超时, 在error_cb里面处理超时。
*. read_cb和write_cb的原型是
void read_or_write_callback(struct bufferevent *bev, void *arg)
error_cb的原型是
void error_cb(struct bufferevent *bev, short error, void *arg) //这个是event的标准回调函数原型
可以从bev中用libevent的API提取出event_base、sockfd、input/output等相关数据,
于是代码简化到只需要几行的read_cb和error_cb函数即可:void read_cb(struct bufferevent *bev, void *arg) { char line[256]; int n; evutil_socket_t fd = bufferevent_getfd(bev); while (n = bufferevent_read(bev, line, 256), n > 0) bufferevent_write(bev, line, n);}void error_cb(struct bufferevent *bev, short event, void *arg) { bufferevent_free(bev);}综合上述,代码如下:
#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <assert.h>#include <event2/event.h>#include <event2/bufferevent.h>#define LISTEN_PORT 9999#define LISTEN_BACKLOG 32void do_accept(evutil_socket_t listener, short event, void *arg);void read_cb(struct bufferevent *bev, void *arg);void error_cb(struct bufferevent *bev, short event, void *arg);void write_cb(struct bufferevent *bev, void *arg);int main(){ //int ret; evutil_socket_t listener; listener = socket(AF_INET, SOCK_STREAM, 0); assert(listener > 0); evutil_make_listen_socket_reuseable(listener); struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_addr.s_addr = 0; sin.sin_port = htons(LISTEN_PORT); if (bind(listener, (struct sockaddr *)&sin, sizeof(sin)) < 0) { perror("bind"); return 1; } if (listen(listener, LISTEN_BACKLOG) < 0) { perror("listen"); return 1; } printf ("Listening...\n"); evutil_make_socket_nonblocking(listener); struct event_base *base = event_base_new(); assert(base != NULL); struct event *listen_event; listen_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base); event_add(listen_event, NULL); event_base_dispatch(base); printf("The End."); return 0;}void do_accept(evutil_socket_t listener, short event, void *arg){ struct event_base *base = (struct event_base *)arg; evutil_socket_t fd; struct sockaddr_in sin; socklen_t slen = sizeof(sin); fd = accept(listener, (struct sockaddr *)&sin, &slen); if (fd < 0) { perror("accept"); return; } if (fd > FD_SETSIZE) { //这个if是参考了那个ROT13的例子,貌似是官方的疏漏,从select-based例子里抄过来忘了改 perror("fd > FD_SETSIZE\n"); return; } printf("ACCEPT: fd = %u\n", fd); struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); bufferevent_setcb(bev, read_cb, NULL, error_cb, arg); bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST);}void read_cb(struct bufferevent *bev, void *arg){#define MAX_LINE 256 char line[MAX_LINE+1]; int n; evutil_socket_t fd = bufferevent_getfd(bev); while (n = bufferevent_read(bev, line, MAX_LINE), n > 0) { line[n] = '\0'; printf("fd=%u, read line: %s\n", fd, line); bufferevent_write(bev, line, n); }}void write_cb(struct bufferevent *bev, void *arg) {}void error_cb(struct bufferevent *bev, short event, void *arg){ evutil_socket_t fd = bufferevent_getfd(bev); printf("fd = %u, ", fd); if (event & BEV_EVENT_TIMEOUT) { printf("Timed out\n"); //if bufferevent_set_timeouts() called } else if (event & BEV_EVENT_EOF) { printf("connection closed\n"); } else if (event & BEV_EVENT_ERROR) { printf("some other error\n"); } bufferevent_free(bev);}
为完整起见,在此分别附上客户端和服务端的代码。
服务端代码:
//server端的代码#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <assert.h>#include <unistd.h>#include <string.h>#include <event2/event.h>#include <event2/bufferevent.h>#define LISTEN_PORT 9999#define LISTEN_BACKLOG 32#define MAX_LINE 256void do_accept(evutil_socket_t listener, short event, void *arg);void read_cb(struct bufferevent *bev, void *arg);void error_cb(struct bufferevent *bev, short event, void *arg);void write_cb(struct bufferevent *bev, void *arg);int main(){ //int ret; evutil_socket_t listener;//用于跨平台表示socket的ID(在Linux下表示的是其文件描述符) listener = socket(AF_INET, SOCK_STREAM, 0); assert(listener > 0); //用于跨平台将socket设置为可重用(实际上是将端口设为可重用 evutil_make_listen_socket_reuseable(listener); struct sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_addr.s_addr = 0; sin.sin_port = htons(LISTEN_PORT); if (bind(listener, (struct sockaddr *)&sin, sizeof(sin)) < 0) { perror("bind"); return 1; } if (listen(listener, LISTEN_BACKLOG) < 0) { perror("listen"); return 1; } printf ("Listening...\n"); /* 用于跨平台将socket设置为非阻塞,使用bufferevent需要 */ evutil_make_socket_nonblocking(listener); //主要记录事件的相关属性 struct event_base *base = event_base_new(); assert(base != NULL); /* Register listen event. */ struct event *listen_event; listen_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base); event_add(listen_event, NULL); /* Start the event loop. */ event_base_dispatch(base); printf("The End."); //close(listener); return 0;}void do_accept(evutil_socket_t listener, short event, void *arg){ struct event_base *base = (struct event_base *)arg; evutil_socket_t fd; struct sockaddr_in sin; socklen_t slen = sizeof(sin); fd = accept(listener, (struct sockaddr *)&sin, &slen); if (fd < 0) { perror("accept"); return; } if (fd > FD_SETSIZE) { //这个if是参考了那个ROT13的例子,貌似是官方的疏漏,从select-based例子里抄过来忘了改 perror("fd > FD_SETSIZE\n"); return; } printf("ACCEPT: fd = %u\n", fd); //关联该sockfd,托管给event_base. struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); //设置读写对应的回调函数 bufferevent_setcb(bev, read_cb, NULL, error_cb, arg); //启用读写事件,其实是调用了event_add将相应读写事件加入事件监听队列poll. //如果相应事件不置为true,bufferevent是不会读写数据的 bufferevent_enable(bev, EV_READ|EV_PERSIST);// 进入bufferevent_setcb回调函数:// 在readcb里面从input中读取数据,处理完毕后填充到output中;// writecb对于服务端程序,只需要readcb就可以了,可以置为NULL;// errorcb用于处理一些错误信息}void read_cb(struct bufferevent *bev, void *arg){ char line[MAX_LINE+1]; int n; evutil_socket_t fd = bufferevent_getfd(bev); while (n = bufferevent_read(bev, line, MAX_LINE), n > 0) { line[n] = '\0'; printf("fd=%u, Server gets the message from client read line: %s\n", fd, line);//直接将读取的结果,不做任何修改(本文是跳过前两个字符),直接返回给客户端bufferevent_write(bev, line+2, n);//方案1 }}void write_cb(struct bufferevent *bev, void *arg){ printf("HelloWorld\n");//直接空代码即可,因为这里并不会被触发调用}void error_cb(struct bufferevent *bev, short event, void *arg){ evutil_socket_t fd = bufferevent_getfd(bev); printf("fd = %u, ", fd); if (event & BEV_EVENT_TIMEOUT) { printf("Timed out\n"); //if bufferevent_set_timeouts() called } else if (event & BEV_EVENT_EOF) { printf("connection closed\n"); } else if (event & BEV_EVENT_ERROR) { printf("some other error\n"); } bufferevent_free(bev);}
客户端代码:
//客户端代码#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <unistd.h>#include <string.h>#include <netinet/in.h>#include <sys/socket.h>#include <arpa/inet.h>#include <sys/types.h>#include <event2/event.h>#include <event2/bufferevent.h>#define SERV_PORT 9999#define MAX_LINE 1024void cmd_msg_cb(int fd, short event, void *arg);void read_cb(struct bufferevent *bev, void *arg);void error_cb(struct bufferevent *bev, short event, void *arg);int main(int argc, char *argv[]){ if(argc < 2) { perror("usage: echocli <IPadress>"); return 1; } evutil_socket_t sockfd; if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket\n"); return 1; } struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) < 1) { perror("inet_ntop\n"); return 1; } if(connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) { perror("connect\n"); return 1; } evutil_make_socket_nonblocking(sockfd); printf("Connect to server sucessfully!\n"); // build event base struct event_base *base = event_base_new(); if(base == NULL) { perror("event_base\n"); return 1; } const char *eventMechanism = event_base_get_method(base); printf("Event mechanism used is %s\n", eventMechanism); printf("sockfd = %d\n", sockfd); struct bufferevent *bev = bufferevent_socket_new(base, sockfd, BEV_OPT_CLOSE_ON_FREE); struct event *ev_cmd; ev_cmd = event_new(base, STDIN_FILENO, EV_READ | EV_PERSIST, cmd_msg_cb, (void *)bev); event_add(ev_cmd, NULL); bufferevent_setcb(bev, read_cb, NULL, error_cb, (void *)ev_cmd); bufferevent_enable(bev, EV_READ | EV_PERSIST); event_base_dispatch(base); printf("The End."); return 0;}void cmd_msg_cb(int fd, short event, void *arg){ char msg[MAX_LINE]; int nread = read(fd, msg, sizeof(msg)); if(nread < 0) { perror("stdio read fail\n"); return; } struct bufferevent *bev = (struct bufferevent *)arg; bufferevent_write(bev, msg, nread);}void read_cb(struct bufferevent *bev, void *arg){ char line[MAX_LINE + 1]; int n; evutil_socket_t fd = bufferevent_getfd(bev); while((n = bufferevent_read(bev, line, MAX_LINE)) > 0) { line[n] = '\0'; printf("fd = %u, read from server: %s", fd, line); }}void error_cb(struct bufferevent *bev, short event, void *arg){ evutil_socket_t fd = bufferevent_getfd(bev); printf("fd = %u, ", fd); if(event & BEV_EVENT_TIMEOUT) printf("Time out.\n"); // if bufferevent_set_timeouts() is called else if(event & BEV_EVENT_EOF) printf("Connection closed.\n"); else if(event & BEV_EVENT_ERROR) printf("Some other error.\n"); bufferevent_free(bev); struct event *ev = (struct event *)arg; event_free(ev);}
运行结果如下所示:本文为了方便区别,将客户端发送到服务端再回显到客户端的过程,在从服务端回显到客户端的时候,去掉前两个字符。如,客户端输入1234567,则从服务端读取到的数据为34567。
客户端的输入和输出结果:
服务端的输入和输出结果:
参考:
http://www.cnblogs.com/cnspace/archive/2011/07/19/2110891.html
http://www.felix021.com/blog/read.php?2068
http://www.bkjia.com/ASPjc/886985.html
http://blog.csdn.net/tujiaw/article/details/8872943
http://popozhu.github.io/2013/06/26/libevent_r5_bufferevent%E5%9F%BA%E7%A1%80%E5%92%8C%E6%A6%82%E5%BF%B5/
http://blog.csdn.net/bestone0213/article/details/46729091
https://www.ibm.com/developerworks/cn/aix/library/au-libev/
- linux下libevent的安装和使用例子:数据回显
- linux下libevent的安装和使用例子:数据回显
- Linux下libevent的安装
- linux下安装libevent和memcache
- 1.Linux下libevent和memcached安装
- Linux下libevent安装
- Linux下libevent安装
- 稳定的libevent使用例子
- libevent的安装和部分使用代码
- linux下安装配置使用memcache,memcached,libevent(后有ubuntu的memcache安装)
- linux下安装配置使用memcache,memcached,libevent(后有ubuntu的memcache安装)
- Linux下的Memcache安装(含libevent的安装)
- linux下安装配置使用(libevent,memcached,memcache)
- Linux下的Memcache安装(附libevent安装)
- Linux下的Memcache安装(附libevent安装)
- linux下使用libevent开发遇到的龌龊问题
- vs2010下libevent的使用
- vs2010下libevent的使用
- 【JS】javascript 小技巧
- js跨域请求简单例子
- java线程池
- 快速排序代码
- Android问题集(二)——TextView在点击时显示不同颜色,Button点击效果
- linux下libevent的安装和使用例子:数据回显
- MFC中UpdateData()函数的使用
- Hadoop学习笔记(十九)---Hive Java Api
- .sqc文件代码审计
- 同时用两个定时器控制蜂鸣器发声,定时器0控制频率,定时器1控制同个频率持续的时间,间隔300ms依次输出1,10,50,100,200,400,800,1K的方波
- 2015 8月 面试记,一些android的面试题
- iOS调试——Instruments
- 《机器学习实战》笔记一——机器学习基础
- EditText控件及相关操作