F-Stack实现UDP服务端客户端,并进行吞吐量测试的实现

Posted rtoax

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了F-Stack实现UDP服务端客户端,并进行吞吐量测试的实现相关的知识,希望对你有一定的参考价值。

Table of Contents

怎么个加速法?

我的网络拓扑

开始编码

服务端

server.c

common.h

Makefile

配置文件server.ini

客户端代码

client.c

common.h

common.c

Makefile

运行结果


最近在做DPDK相关的网口加速的内容,开始采用DPDK+VPP的方式,但鉴于VPP的部署需要花费一定的时间,遂采用F-Stack,关于F-Stack,本文不做介绍,只说明它是腾讯开发的。

鉴于“一图胜过千言”,本文将以图+简短的话为例

怎么个加速法?

F-Stack不走内核协议栈,通过移植BSD的协议栈和DPDK进行联合,组成一套完整的用户态协议栈。

我准备用两个万兆网卡加速,如下图

然后在采用一个网卡运行服务端,一个网卡运行客户端,

也可以总结成这样

可是最后都初始化ff_init后法宝不成功,最终我确定了一个方案

也就是万兆网卡运行客户端进行收发包,内核协议栈控制的千兆网卡运行客户端收发包。

我的网络拓扑

一开始我的拓扑是下面这样

我从我的虚拟机进行对左侧X86的控制,左面服务器运行客户端和服务端,但是好像哪里不对,因为我要让数据像下面这样走

当然是少了一根线,可以有两种连接方法

我踩用第一种,如下

开始编码

https://download.csdn.net/download/Rong_Toa/12631153

服务端

server.c

/**
 *  测试F-Stack的UDP发包速率
 *  作者:荣涛 <rongtao@sylincom.com>
 *  时间:2020年7月16日10:08:34
 */
#include "common.h"

#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <assert.h>

#include <sys/socket.h>
#include <netinet/ip.h>
#include <sys/epoll.h>


int sockfd;

int epfd;
struct epoll_event ev;
struct epoll_event events[MAX_EVENTS];

char buf[MAXLINE] = "100000000";



int udpsocket_server()

    int sockfd;
    
    struct sockaddr_in servaddr;

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;    
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);
    
    if((sockfd = ff_socket(AF_INET, SOCK_DGRAM, 0)) < 0) 
        return -1;
    

    if(ff_bind(sockfd, (struct linux_sockaddr *)&servaddr, sizeof(servaddr))) 
        return -1;
    
    
    return sockfd;


long int direct_sendto_addr(int sockfd, const char *dst_ip, int port, long int n_pkg)

    
	struct sockaddr_in cliaddr;
    int n = 0;
    
    long int npkg = 0, nbyte=0;
    
	socklen_t len;
    len = sizeof(cliaddr);
    struct timeval tvbrfore, tvafter;
    
    bzero(&cliaddr, sizeof(struct sockaddr_in));
    cliaddr.sin_family = AF_INET;
    cliaddr.sin_port = htons(port);
    inet_pton(AF_INET, dst_ip, &cliaddr.sin_addr);

    /* 统计时间 */
    gettimeval(&tvbrfore);

     /* 循环发包,测速率 */
    while(1) 
        if((n = ff_sendto(sockfd, buf, MAXLINE, 0, (struct linux_sockaddr *)&cliaddr, len)) < 0)
        
            log_warn("sendto error");
            exit(1);
         else 
            nbyte += n;
            if(npkg++ > n_pkg) 
                break;
            
        
    
    /* 统计时间 */
    gettimeval(&tvafter);
    
    /* 输出此段时间内的速率 */
    statistic_throughput("Sendto", &tvbrfore, &tvafter, nbyte, npkg-1);


int server_loop(void *arg)

    int n;
	socklen_t len;
    
	struct sockaddr_in cliaddr;
    struct timeval tvbrfore, tvafter;

    
#if HAVE_FSTACK!=1
    while(1) 
        log_warn("Wait for a Client epoll_wait...\\n");
#endif
    
    /* Wait for events to happen */
    int nevents = ff_epoll_wait(epfd,  events, MAX_EVENTS, -1);
    int i;
    len = sizeof(cliaddr);
        
#if HAVE_FSTACK!=1
    log_warn("nevents = %d\\n", nevents);
#endif
    for (i = 0; i < nevents; ++i) 
        /* 如果是UDP的服务端, 接收一个来自客户端的消息,
            获取客户端地址,用于向客户端发送数据测发送速率 */
        if (events[i].data.fd == sockfd) 
    
    
            if((n = ff_recvfrom(sockfd, buf, MAXLINE, 0,  (struct linux_sockaddr *)&cliaddr, &len)) < 0)
            
                log_warn("recvfrom error\\n");
             else 
                log_warn("Server recv   %s\\n", buf);
                log_warn("Client Family %d\\n", cliaddr.sin_family);
                log_warn("Client Port   %u\\n", ntohs(cliaddr.sin_port));
                log_warn("Client Addr   %s\\n", inet_ntoa(cliaddr.sin_addr));

                
                
                long int npkg = 0, nbyte=0;
                long int Nkpg = atol(buf);
#if HAVE_FSTACK==1
                Nkpg = Nkpg>1000000?Nkpg:1000000;
#else
                Nkpg = Nkpg>10000?Nkpg:10000;
#endif
                
                log_warn("Ready Send N Pkg %ld\\n", Nkpg);

                /* 告诉我向客户端发了啥 */
#define MY_ID   "[FSTACK-DPDK][RongTao Test][牛逼了老哥]"
                strcpy(buf, MY_ID);

                /* 统计时间 */
                gettimeval(&tvbrfore);
                
                /* 循环发包,测速率 */
                while(1) 
                    if((n = ff_sendto(sockfd, buf, MAXLINE, 0, (struct linux_sockaddr *)&cliaddr, len)) < 0)
                    
                        log_warn("sendto error");
                        exit(1);
                     else 
                        nbyte += n;
                        if(npkg++ > Nkpg) 
                            break;
                        
                    
                
                /* 统计时间 */
                gettimeval(&tvafter);
                
                /* 输出此段时间内的速率 */
                statistic_throughput("Sendto", &tvbrfore, &tvafter, nbyte, npkg-1);
            
        
    

#if HAVE_FSTACK!=1
    
#endif



int main(int argc, char *argv[])

    /* 绑核 */
    setaffinity(7);

#if HAVE_FSTACK==1

    ff_init(argc, argv);
#endif

    log_warn("Init socket.\\n");
    
    sockfd = udpsocket_server();

    assert((epfd = ff_epoll_create(10)) > 0);
    ev.data.fd = sockfd;
    ev.events = EPOLLIN;
    ff_epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

    log_warn("Sockfd %d Wait for a Client...\\n", sockfd);
    
#if 1

#if HAVE_FSTACK==1
    log_warn("Ready to ff_run.\\n");
    ff_run(server_loop, NULL);
#else
    server_loop(NULL);
#endif
#else //直接发送 
    direct_sendto_addr(sockfd, "10.170.6.66", PORT, 10000);
#endif


common.h

/**
 *  测试F-Stack的UDP发包速率
 *  作者:荣涛 <rongtao@sylincom.com>
 *  时间:2020年7月16日10:08:34
 */
#ifndef __COMMON_H
#define __COMMON_H 1

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/sysinfo.h>
#include <time.h>
#include <stdint.h>


#define __USE_GNU
#include <sched.h>
#include <ctype.h>
#include <string.h>

#include <stdarg.h>
#include <libgen.h>

#include "ff_config.h"
#include "ff_api.h"
#include "ff_epoll.h"

/* 使用 fstack 进行UDP发包速率测试=1 
   使用 linux 进行UDP发包速率测试=1 
*/
#define HAVE_FSTACK 1

#define PORT 2152

#define MAXLINE 1500

#define MAX_EVENTS 10


#define TEST_ADDR1    "10.170.7.169"
#define TEST_ADDR2    "10.170.7.170"


/**
 *  打印log debug信息
 *  作者: 荣涛
 *  时间: s2020年7月15日10:12:24
 */
enum 
    __LV_INFO,
    __LV_WARNING,
    __LV_ERR,
    __LV_DEBUG,
;

#define LOG_DEBUG 1
#ifdef LOG_DEBUG
#define log_info(fmt...) ___debug_log(__LV_INFO, __FILE__, __func__ ,__LINE__, fmt)
#define log_warn(fmt...) ___debug_log(__LV_WARNING, __FILE__, __func__ ,__LINE__, fmt)
#define log_error(fmt...) ___debug_log(__LV_ERR, __FILE__, __func__ ,__LINE__, fmt)
#define log_debg(fmt...) ___debug_log(__LV_DEBUG, __FILE__, __func__ ,__LINE__, fmt)
#else
#define log_info(fmt...) 
#define log_warn(fmt...)
#define log_error(fmt...)
#define log_debg(fmt...)
#define log_errorno(i_errno)
#endif



#if HAVE_FSTACK!=1
#define ff_socket socket
#define ff_bind bind
#define ff_recvfrom recvfrom
#define ff_sendto sendto
#define linux_sockaddr sockaddr
#define ff_epoll_wait epoll_wait
#define ff_epoll_create epoll_create
#define ff_epoll_ctl epoll_ctl
#endif


static inline int ___debug_log(int level, char *file, const char *func, int line, char *fmt, ...)

    

    va_list av;
    va_start(av, fmt);

    switch(level) 
        case __LV_INFO:
            printf(" [%sINFO%s][%s:%s:%d]: ","\\033[1;36m","\\033[0m", basename(file), func, line);
            break;
        case __LV_WARNING:
            printf(" [%sWARN%s][%s:%s:%d]: ","\\033[1;35m","\\033[0m", basename(file), func, line);
            break;
        case __LV_ERR:
            printf("[%sERROR%s][%s:%s:%d]: ","\\033[1;31m","\\033[0m", basename(file), func, line);
            break;
        case __LV_DEBUG:
            printf("[%sDEBUG%s][%s:%s:%d]: ","\\033[1m",   "\\033[0m", basename(file), func, line);
            break;
    
    
    int i = vprintf(fmt, av);

    va_end(av);

    return i;



static inline long int gettimeval(struct timeval *tv)

    gettimeofday(tv, NULL);    

static inline void statistic_throughput(char *description, 
            struct timeval *before, struct timeval *after, unsigned long int bytes, long int npkg
)

//    printf("\\t -- before time: %ld, %ld\\n", before->tv_sec, before->tv_usec);
//    printf("\\t --  after time: %ld, %ld\\n", after->tv_sec, after->tv_usec);
    printf("-- %s: Total %.3lf Mbps, bytes = %ld(bits:%ld), npkg = %ld.\\n", 
                            description?description:"Unknown Description", 
                            8*bytes*1.0/((after->tv_sec-before->tv_sec)*1000000
    						            +after->tv_usec-before->tv_usec), bytes, bytes*8, npkg);



static void setaffinity(long int ncpu)

//    long int ncpu = sysconf (_SC_NPROCESSORS_ONLN);
    
    cpu_set_t cpuset;

	CPU_ZERO(&cpuset);
    
    CPU_SET(ncpu>1?ncpu-1:1, &cpuset);
    
    int ret = sched_setaffinity(getpid(), sizeof(cpuset), &cpuset);
    
    log_warn("setaffinity ret = %d\\n", ret);
    
    int j;
    for(j=0;j<CPU_SETSIZE; j++)
    
        if(CPU_ISSET(j, &cpuset))
        
            printf("CPU_SETSIZE = %d, j = %d, cpuset = %d\\n", CPU_SETSIZE, j, cpuset);
            CPU_CLR(j, &cpuset);
            printf("CPU_SETSIZE = %d, j = %d, cpuset = %d\\n", CPU_SETSIZE, j, cpuset);
        
    
    
    ret = sched_getaffinity(getpid(), sizeof(cpuset), &cpuset);

    for(j=0;j<CPU_SETSIZE; j++)
    
        if(CPU_ISSET(j, &cpuset))
        
            printf("CPU_SETSIZE = %d, j = %d, cpuset = %d\\n", CPU_SETSIZE, j, cpuset);
        
    





#endif /*__COMMON_H*/

Makefile

TOPDIR=..

ifeq ($(FF_PATH),)
	FF_PATH=$TOPDIR
endif

ifeq ($(FF_DPDK),)
	FF_DPDK=$TOPDIR/dpdk/x86_64-native-linuxapp-gcc
endif

LIBS+= -L$FF_PATH/lib -Wl,--whole-archive,-lfstack,--no-whole-archive
LIBS+= -L$FF_DPDK/lib -Wl,--whole-archive,-ldpdk,--no-whole-archive
LIBS+= -Wl,--no-whole-archive -lrt -lm -ldl -lcrypto -pthread -lnuma

all:
	cc -O -gdwarf-2  -I../lib -o server server.c $LIBS

.PHONY: clean
clean:
	rm -f *.o server client

配置文件server.ini

# F-Stack 参数配置 <rongtao@sylincom.com>
[dpdk]
# Hexadecimal bitmask of cores to run on.
# 绑核
lcore_mask=3

# Number of memory channels.
channel=8

# Specify base virtual address to map.
#base_virtaddr=0x7f0000000000

# Promiscuous mode of nic, defualt: enabled.
# 网卡的混杂模式
promiscuous=1
numa_on=1

# TX checksum offload skip, default: disabled.
# We need this switch enabled in the following cases:
# -> The application want to enforce wrong checksum for testing purposes
# -> Some cards advertize the offload capability. However, doesn't calculate checksum.
tx_csum_offoad_skip=0

# TCP segment offload, default: disabled.
tso=0

# HW vlan strip, default: enabled.
vlan_strip=1

# sleep when no pkts incomming
# unit: microseconds
idle_sleep=0

# sent packet delay time(0-100) while send less than 32 pkts.
# default 100 us.
# if set 0, means send pkts immediately.
# if set >100, will dealy 100 us.
# unit: microseconds
pkt_tx_delay=100

# enabled port list
#
# EBNF grammar:
#
#    exp      ::= num_list "," num_list
#    num_list ::= <num> | <range>
#    range    ::= <num>"-"<num>
#    num      ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
#
# examples
#    0-3       ports 0, 1,2,3 are enabled
#    1-3,4,7   ports 1,2,3,4,7 are enabled
#
# If use bonding, shoule config the bonding port id in port_list
# and not config slave port id in port_list
# such as, port 0 and port 1 trank to a bonding port 2,
# should set `port_list=2` and config `[port2]` section
port_list=0,1

# Number of vdev.
nb_vdev=0

# Number of bond.
nb_bond=0

# Each core write into own pcap file, which is open one time, close one time if enough.
# Support dump the first snaplen bytes of each packet.
# if pcap file is lager than savelen bytes, it will be closed and next file was dumped into.
[pcap]
enable = 0
snaplen= 16777216
savelen= 16777216

# Port config section
# Correspond to dpdk.port_list's index: port0, port1...
[port0]
addr=10.170.7.169
netmask=255.255.255.0
broadcast=10.170.7.255
gateway=10.170.7.254

[port1]
addr=10.170.7.170
netmask=255.255.255.0
broadcast=10.170.7.255
gateway=10.170.7.254

# lcore list used to handle this port
# the format is same as port_list
lcore_list=0,1

# bonding slave port list used to handle this port
# need to config while this port is a bonding port
# the format is same as port_list
#slave_port_list=0,1

# Packet capture path, this will hurt performance
#pcap=./a.pcap

# Vdev config section
# orrespond to dpdk.nb_vdev's index: vdev0, vdev1...
#    iface : Shouldn't set always.
#    path : The vuser device path in container. Required.
#    queues : The max queues of vuser. Optional, default 1, greater or equal to the number of processes.
#    queue_size : Queue size.Optional, default 256.
#    mac : The mac address of vuser. Optional, default random, if vhost use phy NIC, it should be set to the phy NIC's mac.
#    cq : Optional, if queues = 1, default 0; if queues > 1 default 1.
#[vdev0]
##iface=/usr/local/var/run/openvswitch/vhost-user0
#path=/var/run/openvswitch/vhost-user0
#queues=1
#queue_size=256
#mac=00:00:00:00:00:01
#cq=0

# bond config section
# See http://doc.dpdk.org/guides/prog_guide/link_bonding_poll_mode_drv_lib.html
#[bond0]
#mode=4
#slave=0000:0a:00.0,slave=0000:0a:00.1
#primary=0000:0a:00.0
#mac=f0:98:38:xx:xx:xx
## opt argument
#socket_id=0
#xmit_policy=l23
#lsc_poll_period_ms=100
#up_delay=10
#down_delay=50

# Kni config: if enabled and method=reject,
# all packets that do not belong to the following tcp_port and udp_port
# will transmit to kernel; if method=accept, all packets that belong to
# the following tcp_port and udp_port will transmit to kernel.
[kni]
enable=1
method=reject
# The format is same as port_list
tcp_port=1-65535
udp_port=1-65535

# FreeBSD network performance tuning configurations.
# Most native FreeBSD configurations are supported.
[freebsd.boot]
hz=100

# Block out a range of descriptors to avoid overlap
# with the kernel's descriptor space.
# You can increase this value according to your app.
fd_reserve=1024

kern.ipc.maxsockets=262144

net.inet.tcp.syncache.hashsize=4096
net.inet.tcp.syncache.bucketlimit=100

net.inet.tcp.tcbhashsize=65536

kern.ncallout=262144

kern.features.inet6=1
net.inet6.ip6.auto_linklocal=1
net.inet6.ip6.accept_rtadv=2
net.inet6.icmp6.rediraccept=1
net.inet6.ip6.forwarding=0

[freebsd.sysctl]
kern.ipc.somaxconn=32768
kern.ipc.maxsockbuf=16777216

net.link.ether.inet.maxhold=5

net.inet.tcp.fast_finwait2_recycle=1
net.inet.tcp.sendspace=16384
net.inet.tcp.recvspace=8192
#net.inet.tcp.nolocaltimewait=1
net.inet.tcp.cc.algorithm=cubic
net.inet.tcp.sendbuf_max=16777216
net.inet.tcp.recvbuf_max=16777216
net.inet.tcp.sendbuf_auto=1
net.inet.tcp.recvbuf_auto=1
net.inet.tcp.sendbuf_inc=16384
net.inet.tcp.recvbuf_inc=524288
net.inet.tcp.sack.enable=1
net.inet.tcp.blackhole=1
net.inet.tcp.msl=2000
net.inet.tcp.delayed_ack=0

net.inet.udp.blackhole=1
net.inet.ip.redirect=0
net.inet.ip.forwarding=0

客户端代码

client.c

/**
 *  测试F-Stack的UDP发包速率
 *  作者:荣涛 <rongtao@sylincom.com>
 *  时间:2020年7月16日10:08:34
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/ip.h>

#include "common.h"

int main(int argc, char *argv[])

    int sockfd, n;
    struct sockaddr_in servaddr;
    socklen_t servlen = sizeof(servaddr);
    
    struct timeval tvbrfore, tvafter;
    
    long int npkg = 0, nbyte=0;
    
    char sendline[MAXLINE], recvline[MAXLINE + 1];
    
    if(argc != 2) 
        perror("Usage: udpcli <IPAddress>");
        exit(1);
    

    sockfd = udpsocket_client(argv[1], &servaddr);
    
    printf("Client Input:");
    
    while(fgets(sendline, MAXLINE, stdin) != NULL)
    
        if(sendto(sockfd, sendline, strlen(sendline), 0, 
                    (struct sockaddr *)&servaddr, servlen) < 0)
        
            perror("sendto error");
            exit(1);
         else 
            printf("Client send %d: %s", sockfd, sendline);
        
        
        npkg = 0, nbyte=0;
        memset(&tvbrfore, 0, sizeof(struct timeval));
        memset(&tvafter, 0, sizeof(struct timeval));
        
        /* 统计时间 */
        my_gettimeval(&tvbrfore);
        
        while(1) 
            if((n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, &servlen)) < 0)
            
                perror("recvfrom error");
                break;
             else 
//                printf("Client recv %d: %s", sockfd, recvline);
                nbyte += n;
                if(npkg++ > 10000) 
                    break;
                

            
            recvline[n] = '\\0';
        
        
        /* 统计时间 */
        my_gettimeval(&tvafter);
        
        /* 输出此段时间内的速率 */
        my_statistic_throughput("Recvfrom", &tvbrfore, &tvafter, nbyte, npkg-1);

        /* 准备下次触发f_stack服务端 进行发包测试 */
        printf(">>");
    

    return 1;

common.h

/**
 *  测试F-Stack的UDP发包速率
 *  作者:荣涛 <rongtao@sylincom.com>
 *  时间:2020年7月16日10:08:34
 */
#ifndef _COMMON_H
#define _COMMON_H

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/sysinfo.h>
#include <time.h>
#include <stdint.h>

#define PORT        2152
#define MAXLINE     1500 

int udpsocket_server();
int udpsocket_client(const char *ipv4, struct sockaddr_in *servaddr);

inline long int my_gettimeval(struct timeval *tv);

inline void my_statistic_throughput(char *description, 
            struct timeval *before, struct timeval *after, unsigned long int bytes, long int npkg);


#endif /*<_COMMON_H>*/

common.c

/**
 *  测试F-Stack的UDP发包速率
 *  作者:荣涛 <rongtao@sylincom.com>
 *  时间:2020年7月16日10:08:34
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/ip.h>

#include "common.h"


int udpsocket_server()

    int sockfd;
    
    struct sockaddr_in servaddr;

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;    
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);
    
    if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
    
        return -1;
    

    if(bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)))
    
        return -1;
    
    
    return sockfd;


int udpsocket_client(const char *ipv4, struct sockaddr_in *servaddr)

    int sockfd, t;
    
    bzero(servaddr, sizeof(struct sockaddr_in));
    servaddr->sin_family = AF_INET;
    servaddr->sin_port = htons(PORT);
    
    if((t = inet_pton(AF_INET, ipv4, &servaddr->sin_addr)) <= 0)
    
        return -1;
    
    if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
    
        return -1;
    
    
    return sockfd;



inline long int my_gettimeval(struct timeval *tv)

    gettimeofday(tv, NULL);    

inline void my_statistic_throughput(char *description, 
            struct timeval *before, struct timeval *after, unsigned long int bytes, long int npkg)

//    printf("\\t -- before time: %ld, %ld\\n", before->tv_sec, before->tv_usec);
//    printf("\\t --  after time: %ld, %ld\\n", after->tv_sec, after->tv_usec);
    printf("-- %s: Total %.3lf Mbps, bytes = %ld(bits:%ld), npkg = %ld.\\n", 
                            description?description:"Unknown Description", 
                            8*bytes*1.0/((after->tv_sec-before->tv_sec)*1000000
    						            +after->tv_usec-before->tv_usec), bytes, bytes*8, npkg);


Makefile

ALL:
	gcc client.c common.c -o client -lm
clean:
	rm *~

运行结果

 

 

 

以上是关于F-Stack实现UDP服务端客户端,并进行吞吐量测试的实现的主要内容,如果未能解决你的问题,请参考以下文章

UDP服务器客户端编程流程

网络编程实验1udp实现CS和端口号

基于UDP用JAVA实现客户端和服务端通信

(dpdk f-stack)-实现L4代理功能

(dpdk f-stack)-实现L4代理功能

C/S模型:TCP,UDP构建客户端和服务器端(BIO实现