epoll多路复用的一个实例程序(C实现)

本文实例为大家分享了epoll多路复用一个实例程序的具体代码,供大家参考,具体内容如下

1、实例程序描述

编写一个echo server程序,功能是客户端向服务端发送消息,服务端接收到消息后输出,并原样返回给客户端,客户端接收到服务端的应答消息并打印输出。

2、公共接口函数部分

2.1、common.h 源文件

/**
**描述:公共头文件
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
 
#include <fcntl.h>
#include <unistd.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
 
typedef struct epoll_event EPOLL_EVENT_T;
 
#define BUF_SIZE 2048
#define LISTENQ  10
#define FD_SEZE  1000
#define MAX_EVENTS 100
 
#define ERROR_SOCKET_SELECT   -1
#define ERROR_SOCKET_TIMEOUT  -2
#define ERROR_SOCKET_READ     -3
#define ERROR_SOCKET_WRITE    -4
#define ERROR_SOCKET_CLOSE    -5
#define ERROR_SOCKET_CREATE   -6
#define ERROR_SOCKET_BIND     -7
#define ERROR_SOCKET_LISTEN   -8
#define ERROR_SOCKET_CONNECT  -9
 
#define ERROR_EPOLL_CREATE    -10
#define ERROR_EPOLL_CTL_ADD   -11
#define ERROR_EPOLL_CTL_DEL   -12
#define ERROR_EPOLL_CTL_MOD   -13
#define ERROR_ARGUMENT         -999
 
int add_epoll_event(int epollfd, int fd, int events);
int del_epoll_event(int epoll, int fd, int events);
int mod_epoll_event(int epoll, int fd, int events);
int make_socket_nonblock(int sock_fd);
int listen_socket(char *ip, int port, int nonblock);
int connect_socket(char *ip, int port,int nonblock);

2.2、common.c 源文件

/**
**描述:公共函数
*/
#include "common.h"
 
int add_epoll_event(int epollfd, int fd, int events)
{
    EPOLL_EVENT_T ev;
    ev.events = events;
    ev.data.fd = fd;
    if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1){
        perror("epoll_ctl_add");
        return ERROR_EPOLL_CTL_ADD;
    }
}
 
int del_epoll_event(int epollfd, int fd, int events)
{
    EPOLL_EVENT_T ev;
    ev.events = events;
    ev.data.fd = fd;
    if(epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, &ev) == -1){
        perror("epoll_ctl_del");
        return ERROR_EPOLL_CTL_ADD;
    }
}
 
int mod_epoll_event(int epollfd, int fd, int events)
{
    EPOLL_EVENT_T ev;
    ev.events = events;
    ev.data.fd = fd;
    if(epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &ev) == -1){
        perror("epoll_ctl_mod");
        return ERROR_EPOLL_CTL_MOD;
    }
}
 
//设置socket为非阻塞模式函数
int make_socket_nonblock(int sock_fd)
{
    int flags;
    
    if((flags = fcntl(sock_fd, F_GETFL, NULL)) < 0){
        printf("get socket fd flags error:%d %s", errno, strerror(errno));
        return -1;
    }
    if(fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK) == -1){
        printf("set socket non-block error:%d %s", errno, strerror(errno));
        return -1;
    }
    
    return 0;
}
 
int listen_socket(char *ip, int port, int nonblock)
{
    int opt=1, sockfd;
    struct sockaddr_in svr_addr;
    
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
        close(sockfd);
        return ERROR_SOCKET_CREATE;
    }
    if(nonblock) //设置socket为非阻塞模式
        make_socket_nonblock(sockfd);
 
    //SO_REUSEADDR是让端口释放后立即就可以被再次使用
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    memset(&svr_addr, 0, sizeof(struct sockaddr_in));
    svr_addr.sin_family = AF_INET;
    if(ip == NULL)
        svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    else
        svr_addr.sin_addr.s_addr = inet_addr(ip);
    svr_addr.sin_port = htons(port);
    
    if(bind(sockfd, (struct sockaddr*)&svr_addr, sizeof(struct sockaddr)) == -1){
        close(sockfd);
        return ERROR_SOCKET_BIND;
    }
    
    if(listen(sockfd, LISTENQ) == -1){
        close(sockfd);
        return ERROR_SOCKET_LISTEN;
    }
    
    return sockfd;
}
 
int connect_socket(char *ip, int port,int nonblock)
{
    int sockfd;
    struct sockaddr_in svr_addr;
    
    if(ip==NULL || strlen(ip)==0 || port<=0)
        return ERROR_ARGUMENT;
    
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0){
        perror("socket");
        return ERROR_SOCKET_CREATE;
    }
    
    memset(&svr_addr, 0, sizeof(svr_addr));
    svr_addr.sin_family = AF_INET;
    svr_addr.sin_addr.s_addr = inet_addr(ip);
    svr_addr.sin_port = htons(port);
 
    if(connect(sockfd, (struct sockaddr*)&svr_addr, sizeof(svr_addr)) == -1){
        perror("connect");
        close(sockfd);
        return ERROR_SOCKET_CONNECT;
    }
    if(nonblock)
        make_socket_nonblock(sockfd);
 
    return sockfd;
}

3、服务端源文件 epoll_server.c

/**
**程序描述:编写一个echo server程序,功能是客户端向服务器发送信息,服务器端接收数据后输出并原样返回给客户端,客户端接收到消息并输出到终端。
*/
#include "common.h"
 
void do_epoll(int listen_fd);
void handle_epoll_events(int epollfd, EPOLL_EVENT_T *events, int nfds, int listen_fd, char *buf);
void on_accept(int epollfd, int listen_fd);
void on_read(int epollfd, int fd, char *buf);
void on_write(int epollfd, int fd, char *buf);
 
int main(int argc, int *argv[])
{
    char svr_ip[32]={0};
    int svr_port;
    int listen_fd, nonblock=0;
    
    if(argc < 3){
        printf("ERROR: too few command-line arguments\n");
        printf("Usage: %s <svr_ip> <svr_port>\n", argv[0]);
        return -1;
    }
    strncpy(svr_ip, argv[1], strlen(argv[1]));
    svr_port = atoi(argv[2]);
    
    listen_fd = listen_socket(svr_ip, svr_port, nonblock);
    if(listen_fd < 0){
        printf("listen socket error:%d %s\n", errno, strerror(errno));
        return -2;
    }
    printf("epoll_svr listen on[%s:%d] succ\n", svr_ip, svr_port);
    do_epoll(listen_fd);
    
    printf("exit epoll_svr succ\n");
    return 0;
}
 
void do_epoll(int listen_fd)
{
    EPOLL_EVENT_T events[MAX_EVENTS];
    int epollfd, conn_fd, nfds;
    char buf[BUF_SIZE]={0};
 
    
    //创建一个epoll文件描述符
    epollfd = epoll_create(FD_SEZE);
    if(epollfd == -1){
        perror("epoll_create");
        close(listen_fd);
        return;
    }
    //添加监听描述符的读事件
    add_epoll_event(epollfd, listen_fd, EPOLLIN);
    
    for(;;){
        nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if(nfds == -1){
            perror("epoll_wait");
            close(listen_fd);
            break;
        }
        handle_epoll_events(epollfd, events, nfds, listen_fd, buf);
    }
    close(epollfd);
}
 
void handle_epoll_events(int epollfd, EPOLL_EVENT_T *events, int nfds, int sockfd, char *buf)
{
    int i=0;
    int fd;
 
    //printf("nfds = %d\n", nfds);
    for(i=0; i < nfds; i++){
        fd = events[i].data.fd;
        //根据文件描述符类型和事件类型进行相应处理
        if(fd == sockfd && (events[i].events & EPOLLIN))
            on_accept(epollfd, sockfd);
        else if(events[i].events & EPOLLIN)
            on_read(epollfd, fd, buf);
        else if(events[i].events & EPOLLOUT)
            on_write(epollfd, fd, buf);
    }
}
 
void on_accept(int epollfd, int listen_fd)
{
    int conn_fd;
    struct sockaddr_in cli_addr;
    socklen_t cli_addr_len=sizeof(struct sockaddr);
    
    conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_addr_len);
    if(conn_fd == -1){
        perror("accept");
    }
    else{
        printf("accept a new client:[%s:%d], conn_fd=%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), conn_fd);
        //make_socket_nonblock(conn_fd);
        add_epoll_event(epollfd, conn_fd, EPOLLIN); //添加一个客户端连接描述符的读事件
    }
}
 
void on_read(int epollfd, int fd, char *buf)
{
    int nread;
    
    nread = read(fd, buf, BUF_SIZE);
    if(nread == -1){
        perror("read");
        close(fd);
        del_epoll_event(epollfd, fd, EPOLLIN);
    }
    else if(nread == 0){
        printf("ERROR:client close\n");
        close(fd);
        del_epoll_event(epollfd, fd, EPOLLIN);
    }
    else{
        printf("recv req msg from client succ, fd=%d, msg:%s", fd, buf);
        //修改描述符对应的事件,由读改为写
        mod_epoll_event(epollfd, fd, EPOLLOUT);
    }
}
 
void on_write(int epollfd, int fd, char *buf)
{
    int nwrite;
    
    nwrite=write(fd, buf, strlen(buf));
    if(nwrite == -1){
        perror("write");
        close(fd);
        del_epoll_event(epollfd, fd, EPOLLOUT);
    }
    else{
        printf("send resp msg to client succ, fd=%d, msg:%s\n", fd, buf);
        mod_epoll_event(epollfd, fd, EPOLLIN); //将描述符对应的事件,由写改为读
    }
    memset(buf, 0, sizeof(buf));
}

4、客户端源文件 epoll_client.c

/**
**程序描述:回射程序echo客户端。客户端也要使用epoll实现对路复用,控制STDIN_FILENO、STDOUT_FILENO和sockfd这三个描述符对应的事件。
STDIN_FILENO:标准输入描述符,只有一个读事件
STDOUT_FILENO:标准输出描述符,只有一个写事件
sockfd: 有两个事件,一个是读事件,即读取从服务端发送来的数据;另一个是写事件,即发送数据给服务端。需要注意的是,在同一时刻,它只能有一个事件
*/
#include "common.h"
 
void do_epoll(int sockfd);
void handle_epoll_events(int epollfd, EPOLL_EVENT_T *events, int nfds, int sockfd, char *buf);
void on_read(int epollfd, int fd, int sockfd, char *buf);
void on_write(int epollfd, int fd, int sockfd, char *buf);
 
int main(int argc, char *argv[])
{
    char svr_ip[32]={0};
    int svr_port;
    int conn_fd, nonblock=0;
    
    
    if(argc < 3){
        printf("ERROR: too few command-line arguments\n");
        printf("Usage: %s <svr_ip> <svr_port>\n", argv[0]);
        return -1;
    }
    strncpy(svr_ip, argv[1], strlen(argv[1]));
    svr_port = atoi(argv[2]);
    
    conn_fd=connect_socket(svr_ip, svr_port, nonblock);
    if(conn_fd < 0){
        printf("connect to server failed!\n", conn_fd);
        return -2;
    }
    printf("connect to server succ, sockfd=%d\n", conn_fd);
    do_epoll(conn_fd);
    
    printf("exit epoll_cli succ\n");
    return 0;
}
 
void do_epoll(int sockfd)
{
    int epollfd;
    EPOLL_EVENT_T events[MAX_EVENTS];
    int nfds;
    char buf[BUF_SIZE]={0};
 
    epollfd = epoll_create(FD_SEZE);
    add_epoll_event(epollfd, STDIN_FILENO, EPOLLIN); //注册标准输入读事件
    for(;;){
        nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if(nfds == -1){
            perror("epoll_wait");
            close(sockfd);
            break;
        }
        handle_epoll_events(epollfd, events, nfds, sockfd, buf);
    }
    close(epollfd);
}
 
void handle_epoll_events(int epollfd, EPOLL_EVENT_T *events, int nfds, int sockfd, char *buf)
{
    int i, fd;
 
    //printf("nfds = %d\n", nfds);
    for(i=0; i<nfds; i++){
        fd = events[i].data.fd;
        //根据文件描述符类型和事件类型进行相应处理
        if(events[i].events & EPOLLIN)
            on_read(epollfd, fd, sockfd, buf);
        else if(events[i].events & EPOLLOUT)
            on_write(epollfd, fd, sockfd, buf);
    }
}
 
void on_read(int epollfd, int fd, int sockfd, char *buf)
{
    int nread;
    
    nread = read(fd, buf, BUF_SIZE);
    if(nread < 0){
        perror("read error");
        close(fd);
    }
    else if(nread == 0){
        printf("epoll_svr close.\n");
        close(fd);
        exit(-1);
    }
    else{
        if(fd == STDIN_FILENO){ //标准输入描述符的读事件
            printf("stdin buf=%s", buf);
            add_epoll_event(epollfd, sockfd, EPOLLOUT); //注册sockfd的写事件
        }
        else{//sockfd描述符的读事件
            del_epoll_event(epollfd, sockfd, EPOLLIN); //删除当前sockfd描述符的读事件
            add_epoll_event(epollfd, STDOUT_FILENO, EPOLLOUT); //注册标准输出写事件
        }
    }
}
 
void on_write(int epollfd, int fd, int sockfd, char *buf)
{
    int nwrite;
    
    nwrite = write(fd, buf, strlen(buf));
    if(nwrite < 0){
        perror("write error");
        close(fd);
    }
    else{
        if(fd == STDOUT_FILENO) //标准输出描述符的写事件
        {
            printf("recv resp_msg from svr, msg=%s\n", buf);
            del_epoll_event(epollfd, fd, EPOLLOUT); //删除输出描述符的写事件
        }
        else //sockfd描述符的写事件
        {
            printf("send req_msg to svr succ, msg=%s", buf);
            mod_epoll_event(epollfd, fd, EPOLLIN); //修改sockfd描述符为读事件
        }
    }
    memset(buf, 0, BUF_SIZE);
}

5、Makefile文件

#第1种方式
 
all: epoll_server epoll_client
 
epoll_server: epoll_server.o common.o
    $(LINK)
epoll_client: epoll_client.o common.o
    $(LINK)
 
%.o: %.c
    $(COMPILE)
 
#compile & link
CFLAGS += -g
COMPILE=gcc -c -g -std=gnu99 -o $@ $<
LINK=gcc -g -o $@ $^
 
clean:
    rm -rf *.o epoll_server epoll_client

6、总结分析

6.1 客户端程序分析

1、对于客户端程序而言,我们监听3个文件描述符,分别是连接服务端的sockfd,标准输入描述符STDIN_FILENO 以及 标准输出描述符STDOUT_FILENO。在 do_poll函数中,我们首先注册了标准输入描述符的读事件(EPOLLIN),然后在for循环中,循环调用epoll_wait系统调用,handle_epoll_events是整个epoll事件表的handler函数。当我们向终端输入数据完毕的时候,就会触发标准输入描述符的读事件,从而调用读事件处理函数on_read,在on_read函数中,注册了sockfd的写事件。

2、在do_poll的for循环中,继续调用epoll_wait 和 handle_epoll_events,发现sockfd的写事件已就绪,转向调用写事件处理函数on_write。在on_write函数中,发送消息给服务端,然后修改sockfd描述符的读事件就绪,准备接收服务端发来的应答消息。

3、在do_poll的for循环中,继续调用epoll_wait 和 handle_epoll_events,发现sockfd的读事件已就绪,转向调用写事件处理函数on_write。在on_read函数中,删除当前sockfd描述符的读事件并注册标准输出描述符的写事件。之所以要删除掉sockfd的读事件,是避免其一直处于就绪状态。

4、在do_poll的for循环中,继续调用epoll_wait 和 handle_epoll_events,发现标准输出描述符的写事件已就绪,转向调用写事件处理函数on_write。在on_write函数中,输出服务端发来的应答消息并删除标准输出描述符的写事件。之所以要删除掉标准输出描述符的写事件,还是为了避免其一直处于就绪状态,具体表现就是不停地在终端打印信息。

6.2 服务端程序分析

1、对于服务端程序而言,我们监听2个文件描述符,分别是接受多个客户端连接请求的listen_fd和处理单个客户端的数据读写的conn_fd。

2、在main函数中,首先创建了监听连接请求的listen_fd文件描述符。然后在do_poll函数中,首先注册了listen_fd描述符的读事件(EPOLLIN)。在for循环中,循环调用epoll_wait系统调用和epoll事件handler函数handle_epoll_events。

3、当有客户端发起连接请求时,会触发listen_fd描述符的读事件,转向执行listen_fd的读事件处理函数on_accept。在on_accept函数中,客户端与服务端成功建立连接,并返回一个conn_fd文件描述符,然后注册这个描述符的读事件。

4、在do_poll的for循环中,继续调用epoll_wait 和 handle_epoll_events,当客户端有数据发送给服务端时,触发conn_fd的读事件,转向执行on_read函数。在on_read函数中,接收客户端发来的消息并在终端输出,然后修改conn_fd描述符为写事件就绪。如果接收到的数据大小为0,则说明连接已经断开,则关闭conn_fd描述符并删除其读事件。

5、在do_poll的for循环中,继续调用epoll_wait 和 handle_epoll_events,发现conn_fd的写事件就绪,转向执行on_write函数。在on_write函数中,发送应答消息给客户端,并修改conn_fd描述符的读事件就绪。

题外话

本epoll实例程序,本人已经在CentOS 7.6系统下测试通过了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Linux IO多路复用之epoll网络编程

    前言 本章节是用基本的Linux基本函数加上epoll调用编写一个完整的服务器和客户端例子,可在Linux上运行,客户端和服务端的功能如下: 客户端从标准输入读入一行,发送到服务端 服务端从网络读取一行,然后输出到客户端 客户端收到服务端的响应,输出这一行到标准输出 服务端 代码如下: #include <unistd.h> #include <sys/types.h> /* basic system data types */ #include <sys/socket.h&

  • IO多路复用之epoll全面总结(必看篇)

    1.基本知识 epoll是在2.6内核中提出的,是之前的select和poll的增强版本.相对于select和poll来说,epoll更加灵活,没有描述符限制.epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次. 2.epoll接口 epoll操作过程需要三个接口,分别如下: #include <sys/epoll.h> int epoll_create(int size); int epoll_ctl(

  • epoll多路复用的一个实例程序(C实现)

    本文实例为大家分享了epoll多路复用一个实例程序的具体代码,供大家参考,具体内容如下 1.实例程序描述 编写一个echo server程序,功能是客户端向服务端发送消息,服务端接收到消息后输出,并原样返回给客户端,客户端接收到服务端的应答消息并打印输出. 2.公共接口函数部分 2.1.common.h 源文件 /** **描述:公共头文件 */ #include <stdio.h> #include <string.h> #include <stdlib.h> #in

  • bat脚本实例实现只允许运行一个实例(安装程序、创建快捷方式脚本)

    复制代码 代码如下: ;我的第一个安装脚本!include "MUI2.nsh"!define DIR "D:\workspace\nsis\files" Name "安装程序"Icon "${DIR}\setup128.ico"OutFile "setup.exe"InstallDir "$PROGRAMFILES\kaserv"RequestExecutionLevel admin

  • 让应用程序只运行一个实例的实现方法

    在我们的程序当中如果要实现类似<360软件管家>的功能,就要解决两个问题,首先是要判断该程序已有一个实例在运行,其次是要将已运行的应用程序实例激活,同时退出第二个应用程序实例. 对于第一个问题,我们可以通过设置命名互斥对象或命名信标对象,在程序启动的时候检测互斥对象或信标对象,如互斥对象或信标对象已存在,则可以判断此程序已有一个实例正在运行. 第二个问题是如何找到已经运行的应用程序实例,如果我们能够找到已运行实例主窗口的指针,即可调用SetForegroundWindow来激活该实例.我们可以

  • 解决C#程序只允许运行一个实例的几种方法详解

    本文和大家讲一下如何使用C#来创建系统中只能有该程序的一个实例运行.要实现程序的互斥,通常有下面几种方式,下面用 C# 语言来实现:方法一:使用线程互斥变量. 通过定义互斥变量来判断是否已运行实例.把program.cs文件里的Main()函数改为如下代码: 复制代码 代码如下: using System;using System.Windows.Forms;using System.Runtime.InteropServices;namespace NetTools{    static cl

  • C# WinForm 判断程序是否已经在运行,且只允许运行一个实例,附源码

    我们开发WinFrom程序,很多时候都希望程序只有一个实例在运行,避免运行多个同样的程序,一是没有意义,二是容易出错. 为了更便于使用,笔者整理了一段自己用的代码,可以判断程序是否在运行,只运行一个实例,而且能实现当程序在运行时,再去双击程序图标,直接呼出已经运行的程序. 下面看代码,只需在程序的入口文件中加如下代码即可: static class Program { /// <summary> /// 应用程序的主入口点. /// </summary> [STAThread] s

  • 在CoreOS上搭建一个WordPress程序操作实例

    CoreOS是一个专门为大规模服务器部署定制的Linux精简系统,它将操作系统和应用程序完全分离,从而降低操作系统和应用程序的耦合度,同时解决了现有Linux服务器在容器资源.权限管理方面出现的问题.就目前来说,CoreOS会是未来操作系统的发展趋势. 那你有没有亲自在CoreOS上部署一个应用程序呢?相信大多数人都没有过这样的经验,在CoreOS上建立一个应用程序可以说是非常辛苦及沮丧的.因为在开始建立程序之前你首先必须了解所有不同的技术. 下面,我们将手把手地教你来创建一个简单的WordPr

  • java如何创建一个jdbc程序详解

    JDBC简介 Java数据库连接(Java Database Connectivity,JDBC),是一种用于执行SQL语句的Java API,它由一组用Java编程语言编写的类和接口组成. JDBC为数据库开发人员提供了一个标准的API,使他们能够用纯Java API来编写数据库应用程序. 使用JDBC编写的程序能够自动地将SQL语句传送给相应的数据库管理系统. JDBC扩展了Java的功能,由于Java语言本身的特点,使得JDBC具有简单.健壮.安全.可移植.获取方便等优势. 我们在没有JD

  • Python聊天室实例程序分享

    上一篇 我们学习了简单的Python TCP Socket 编程,通过分别写服务端和客户端的代码了解基本的 Python Socket 编程模型.本文再通过一个例子来加强一下对 Socket 编程的理解. 一.聊天室程序需求 我们要实现的是简单的聊天室的例子,就是允许多个人同时一起聊天,每个人发送的消息所有人都能接收到,类似于 QQ 群的功能,而不是点对点的 QQ 好友之间的聊天.如下图: 我们要实现的有两部分: Chat Server:聊天服务器,负责与用户建立 Socket 连接,并将某个用

  • Call 从一个批处理程序调用另一个批处理程序,并且不终止父批处理程序。

    Call 从一个批处理程序调用另一个批处理程序,并且不终止父批处理程序.call 命令接受用作调用目标的标签.如果在脚本或批处理文件外使用 Call,它将不会在命令行起作用. 语法 call [[Drive:][Path] FileName [BatchParameters]] [:label [arguments]] 参数 [Drive:}[Path] FileName  指定要调用的批处理程序的位置和名称.filename 参数必须具有 .bat 或 .cmd 扩展名.  BatchPara

  • Actionscript与javascript交互实例程序(修改)

    mxml页面: <?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" width=&

随机推荐