zoukankan      html  css  js  c++  java
  • Linux 高性能服务器编程——Linux网络编程基础API

    问题聚焦:
        这节介绍的不仅是网络编程的几个API
        更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系。
        这节主要介绍三个方面的内容:套接字(socket地址)API,socket基础API,和网络信息API。



    套接字API
    套接字socket:(ip, port),即IP地址和端口对,唯一地表示了使用该TCP通信的一端。

    需要了解:主机字节序和网络字节序。
    原因:考虑32位的机器,CPU的累加器一次装载4字节的内容。那么这4字节在内存中的顺序将影响它被累加器装载后的所代表的含义。
    分类:
    • 大端字节序:“高低,低高”,即,一个整数的高位字节(23~31位)存储在内存的低地址处,低位字节存储在内存的高地址处。
    • 小端字节序:“高高,低低”。
    主机字节序:小端字节序。
    网络字节序:大端字节序。
    存在的问题:并不是所有的机器都采用主机字节序。一旦收发双方的字节序不同,必然导致错误的接收。
    解决方法:发送端总是把要发送的数据转化成大端字节序,再发送。
    注意:即使同一台机器上的两个进程,也要考虑字节序的问题,如一个客户端使用C语言编写,而另一个采用java编写(java虚拟机使用大端字节序)。

    Linux提供的转换函数:
    #inlcude<netinet/in.h>
    unsigned long int htonl( unsigned long int hostlong );
    unsigned short int htons( unsigned short int hostshort );
    unsigned long int ntohl( unsigned long int netlong );
    unsigned short int ntohs( unsigned short int netshort );

    通用socket地址(结构体)
    #include <bits/socket.h>
    struct sockaddr
    {
        sa_family_t sa_family;
        char sa_data[14];
    }
    sa_family_da:地址族类型,与协议族类型对应。取值和对应关系如下图所示:

    sa_data(char):存放socket地址。
    协议族及其地址值:

    数组长度有限,所以Linux提供了下面的新的通用socket地址结构体。
    #include <bits/socket.h>
    struct sockaddr_storage
    {
        sa_family_t sa_family;
        unsigned long int __ssalign;
        char __ss_padding[128-sizeof(__ss__align)];
    }
    __ss__align成员的作用:内存对齐。

    专用socket地址
    既然有通用socket地址,那么自然就有专用的socket地址。
    专用:针对各个协议族的。
    分类:
    • UNIX本地域协议族专用socket地址
    #inlcude<sys/un.h>
    struct sockaddr_un
    {
        sa_family_t sin_family;            /* 地址族:AF_UNIX  */
        char sun_path[108];               /* 文件路径名 */
    };

    • IPv4专用socket地址
    #inlcude<sys/un.h>
    struct sockaddr_in
    {
        sa_family_t sin_family;            /* 地址族:AF_INET  */
        u_int16_t sin_port;
        struct int_addr sin_addr;               /* 文件路径名 */
    };
    struct in_addr
    {
        u_int32_t s_addr;
    };

    • IPv6专用socket地址
    #inlcude<sys/un.h>
    struct sockaddr_in6
    {
        sa_family_t sin6_family;                /* 地址族:AF_INET6*/
        u_int16_t sin_port;                        /* 端口号,要用网络字节序表示 */
        u_int32_t sin6_flowinfo;                /* 流信息,应设置为0  */
        struct int6_addr sin6_addr;           /* IPv6地址结构体 */
        u_int32_t sin6_scope_id;               / * scope Id.,尚处于试验阶段  */
    };
    struct in_addr
    {
        unsigned char sa_addr[16];     /* IPv6地址,要用网络字节序表示  */
    };

           虽然Linux给我们提供了专用的socket地址。但是,在实际使用时,都要强制转换为通用socket地址类型因为所有socket编程接口使用的地址参数的类型都是sockaddr。

    IP地址转换函数
    #include<arpa/inet.h>
    in_addr_t inet_addr( const char* strptr );        /* 点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。失败时返回INADDR_NONE */
    int inet_aton( const char* cp, struct in_addr* inp );        /* 完成和in_addr_t同样的功能,但是将转化结果存储于参数inp指向的地址结构中。 成功时返回1, 失败时返回0 */
    char* inet_ntoa( struct in_addr in );        /* inet_ntoa函数将网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址 */

    有一点需要注意,看下面的代码
    char* szValue1 = inet_ntoa( "1.2.3.4" );
    char* szValue2 = inet_ntoa( "10.194.71.60" );
    printf( "address 1: %s
    ", szValue1 );
    printf( "address 2: %s
    ", szValue2 );
    
    //打印结果
    address1: 10.194.71.60
    address2: 10.194.71.60

    原因:inet_ntoa是不可重入的。因为该函数内部用一个固定的静态变量存储结果,函数的返回值指向该内存。每次调用,都是写入相同的静态内存区,所以不可以多次计算。

    同时适用于IPv4和IPv6地址的转换函数
    #include <arpa/inet.h>
    int inet_pton(int af, const char* src, void* dst);
    const char* inet_ntop( int af, const void* src, char* dst, socklen, cnt);

    函数说明:
    inet_pton函数将用字符串表示的IP地址stc(用点分十进制表示的IPv4地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结果可存储与dst指向的内存中。
    af: 指定地址族,可以是AF_INET或AF_INET6
    inet_pton成功返回1,失败返回0并设置errno

    inet_ntop函数进行相反的转换,前三个参数含义与inet_pton的参数相同,最后一个参数cnt指定目标存储单元的大小。
    下面的两个宏能帮助我们指定这个大小
    #include <netinet/in.h>
    #define INET_ADDRSTRLEN 16
    #define INET6_ADDRSTRLEN 46

    inet_ntop成功时返回目标存储单元的地址,失败则返回NULL并设置errno。 



    创建socket
    UNIX/Linux的一个哲学是:所有东西都是文件。socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符。

    创建一个socket
    #include <sys/types.h>
    #include <sys/socket.h>
    int scoket( int domain, int type, int protocol );

    函数说明:
    domain: 告诉系统使用哪个底层协议。对TCP/IP 协议族而言,该参数应该设置为PF_INET(用于IPv4)或PF_INET6(用于IPv6); 对于UNIX本地域协议族而言,该参数应该设置为PF_UNIX 。 
    type: 指定服务类型。服务类型主要有SOCK_STREAM服务(流服务,使用TCP协议)和SOCK_UGRAM(数据报,使用UDP协议)服务。对于TCP/IP协议族而言,其值取SOCK_STREAM(TCP协议)、SOCK_DGRAM(UDP协议)。
             还可以接受两个标志:SOCK_NONBLOCK(将新创建的socket设为非阻塞)和SOCK_CLOEXEC(用fork调用创建子进程时在子进程中关闭该socket)
    protocol:在前两个参数构成的协议集合下,再选择一个具体的协议。几乎所有情况,把它设置为0.
    成功时返回socket文件描述符,失败则返回-1并设置errno。



    绑定socket
    绑定:将一个上面创建的socket与一个具体的socket地址绑定
    只有绑定了socket之后,客户端才能知道该如何连接它。
    调用:bind
    #include <sys/types.h>
    #include <sys/socket.h>
    int bind( int sockfd, const struct sockaddr* my_addr, socklen_t addrlen );

    函数说明:
    将my_addr所指的socket地址分配给未命名的sockfd文件描述符
    addrlen:指出该socket地址的长度
    成功时返回0,失败时返回-1并设置errno
    其中两种常见的errno是EACCES 和 EADDRINUSE,它们的含义分别是:
    • EACCES : 被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务器端口(端口号为 0~1023)上时,bind将返回EACCES 错误。
    • EADDRINUSE:被绑定的地址正在使用中。比如讲socket绑定到一个处于TIME_WAIT状态的socket地址。



    监听socket
    监听:socket绑定后依然不能马上接受客户连接,我们需要创建一个监听队列以存放待处理的客户连接。
    调用:listen
    #include <sys/socket.h>
    int listen( int sockfd, int backlog );
    函数说明:
    sockfd:被监听的socket
    backlog:提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED 错误信息。在内核版本2.2之前的linux中,backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限,在内核版本2.2之后,它表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog 内核参数定义。backlog典型值是5,通常监听队列中完整连接的上限通常币backlog值略大。
    成功时返回0,失败时返回-1并设置errno。
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <signal.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <assert.h>
    #include <stdio.h>
    #include <string.h>
    
    static int stop = 0;
    static void handle_term( int sig )
    {
        stop = 1;
    }
    
    int main( int argc, char* argv[] )
    {
        signal( SIGTERM, handle_term );
    
        if( argc <= 3 )
        {
            printf( "usage: %s ip_address port_number backlog
    ", basename( argv[0] ) );
            return 1;
        }
        const char* ip = argv[1];
        int port = atoi( argv[2] );
        int backlog = atoi( argv[3] );
    
        int sock = socket( PF_INET, SOCK_STREAM, 0 );
        assert( sock >= 0 );
    
        struct sockaddr_in address;
        bzero( &address, sizeof( address ) );
        address.sin_family = AF_INET;
        inet_pton( AF_INET, ip, &address.sin_addr );
        address.sin_port = htons( port );
    
        int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
        assert( ret != -1 );
    
        ret = listen( sock, backlog );
        assert( ret != -1 );
    
        while ( ! stop )
        {
            sleep( 1 );
        }
    
        close( sock );
        return 0;
    }
    运行:
    #./telnetlisten 10.8.56.206 12345 5  //监听12345端口 backlog取值为5
    #telnet 10.8.56.206 12345 //运行10次
    #netstat -nt | grep 12345

              
    发现处于ESTABLISHED 状态的连接有6个(backlog加1 ),其它的4个连接处于SYN_RCVD 状态。 最终发现完全连接最多有(backlog +1 )个,在不同的系统下,运行结果可能有区别,不过监听队列中完全连接的上限通常比backlog值略大。



    接受连接
    调用:accept
    #include <sys/types.h>
    #include <sys/socket.h>
    int accept( int sockfd, struct sockaddr *addr, socklen_t *addrlen );

    作用:从listen监听队列中取出一个连接。服务器可以通过读写该socket来与被接受连接对应的客户端通信。
    函数说明:
    sockfd:处于监听状态的socket。
    addr:用来获取被接受连接的远端socket地址。
    addrlen:该socket地址的长度由addrlen指定。
    成功返回0,失败返回-1并设置errno。

    现在考虑如下情况:如果监听队列中处于ESTABLISHED 状态的连接对应的客户端出现网络异常(比如掉线),或者提前退出,那么服务器对这个连接执行的accept调用是否成功?
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <assert.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    
    int main( int argc, char* argv[] )
    {
        if( argc <= 2 )
        {
            printf( "usage: %s ip_address port_number
    ", basename( argv[0] ) );
            return 1;
        }
        const char* ip = argv[1];
        int port = atoi( argv[2] );
    
        struct sockaddr_in address;
        bzero( &address, sizeof( address ) );
        address.sin_family = AF_INET;
        inet_pton( AF_INET, ip, &address.sin_addr );
        address.sin_port = htons( port );
    
        int sock = socket( PF_INET, SOCK_STREAM, 0 );
        assert( sock >= 0 );
    
        int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
        assert( ret != -1 );
    
        ret = listen( sock, 5 );
        assert( ret != -1 );
    	// 暂停20s 以等待客户端连接和相关操作(掉线或者退出)完成
        sleep(20);
        struct sockaddr_in client;
        socklen_t client_addrlength = sizeof( client );
        int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
        if ( connfd < 0 )
        {
            printf( "errno is: %d
    ", errno );
        }
        else
        {
            char remote[INET_ADDRSTRLEN ];
            printf( "connected with ip: %s and port: %d
    ", 
                inet_ntop( AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN ), ntohs( client.sin_port ) );
            close( connfd );
        }
    
        close( sock );
        return 0;
    }
    具体操作如下:
    #./accept 10.8.56.206 12345
    #telnet 10.8.56.206 12345
    在启动telnet客户端程序后,在20s内关闭telnet 客户端。结果发现accept调用能够正常返回,服务器输出如下:
    [root@vm MOTO]# ./accept 10.8.56.206 12345
    connected with ip: 10.8.56.201 and port: 1313

    由此可见,accept 只是从监听队列中取出连接,而不论连接处于何种状态(如上图CLOSE_WAIT状态或者或者ESTABLISHED状态(断开telent客户端与服务器的网络,服务器该连接的状态)),更不关心任何网络状态的变化。




    发起连接
    调用:connect
    #include <sys/types.h>
    #include <sys/socket.h>
    int connect( int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen );

    函数说明:
    sockfd:由socket调用返回一个socket
    serv_addr:服务器监听的socket地址
    addrlen:指定这个地址的长度
    成功时返回0,失败则返回-并设置errno
    其中两种常见的errno是ECONNREFUSED和ETIMEDOUT,它们的含义如下:
    • ECONNREFUSED:目标端口不存在,连接被拒绝。
    • ETIMEDOUT:连接超时。



    关闭连接
    关闭:close
    #include <unist.h>
    int close ( int fd );

    函数说明:
    fd:待关闭的socket。
    注意:
    close系统调用并非立即关闭一个连接,而是将fd的引用计数减1,只有当fd的引用计数为0时,才真正关闭连接。
    多进程程序中,一次fork调用默认使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程两种都对该socket执行close调用才能将该连接关闭。

    为了避免上面的麻烦,对于无论如何都要立即关闭连接,可以使用下面的调用:、
    #include <sys/socket.h>
    int shutdown ( int sockfd, int howto );

    函数说明
    sockfd:等待关闭的socket
    howto:决定了shutdown的行为。可选值如下表:
        


    数据读写

    用于TCP流数据读写的调用:
    #include <sys/types.h>
    #include <sys/socket.h>
    ssize_t recv ( int sockfd, void *buf, size_t len, int flags );
    ssize_t send ( int sockfd, const void *buf, size_t len, int flags );

    函数说明:
    recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags一般设置为0。返回0说明连接关闭,返回-1出错并设置errno。recv可能返回0,这意味着通信对方已经关闭连接了。
    send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。成功时返回实际写入的数据的长度,失败则返回-1并设置errno。
               
    MSG_OOB选项给应用程序提供了发送和接收带外数据的方法:
    发送方:
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <assert.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    
    int main( int argc, char* argv[] )
    {
        if( argc <= 2 )
        {
            printf( "usage: %s ip_address port_number
    ", basename( argv[0] ) );
            return 1;
        }
        const char* ip = argv[1];
        int port = atoi( argv[2] );
    
        struct sockaddr_in server_address;
        bzero( &server_address, sizeof( server_address ) );
        server_address.sin_family = AF_INET;
        inet_pton( AF_INET, ip, &server_address.sin_addr );
        server_address.sin_port = htons( port );
    
        int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
        assert( sockfd >= 0 );
        if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
        {
            printf( "connection failed
    " );
        }
        else
        {
            printf( "send oob data out
    " );
            const char* oob_data = "abc";
            const char* normal_data = "123";
            send( sockfd, normal_data, strlen( normal_data ), 0 );
            send( sockfd, oob_data, strlen( oob_data ), MSG_OOB );
            send( sockfd, normal_data, strlen( normal_data ), 0 );
        }
    
        close( sockfd );
        return 0;
    }

    接收方:
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <assert.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <string.h>
    
    #define BUF_SIZE 1024
    
    int main( int argc, char* argv[] )
    {
        if( argc <= 2 )
        {
            printf( "usage: %s ip_address port_number
    ", basename( argv[0] ) );
            return 1;
        }
        const char* ip = argv[1];
        int port = atoi( argv[2] );
    
        struct sockaddr_in address;
        bzero( &address, sizeof( address ) );
        address.sin_family = AF_INET;
        inet_pton( AF_INET, ip, &address.sin_addr );
        address.sin_port = htons( port );
    
        int sock = socket( PF_INET, SOCK_STREAM, 0 );
        assert( sock >= 0 );
    
        int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
        assert( ret != -1 );
    
        ret = listen( sock, 5 );
        assert( ret != -1 );
    
        struct sockaddr_in client;
        socklen_t client_addrlength = sizeof( client );
        int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
        if ( connfd < 0 )
        {
            printf( "errno is: %d
    ", errno );
        }
        else
        {
            char buffer[ BUF_SIZE ];
    
            memset( buffer, '', BUF_SIZE );
            ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
            printf( "got %d bytes of normal data '%s'
    ", ret, buffer );
    
            memset( buffer, '', BUF_SIZE );
            ret = recv( connfd, buffer, BUF_SIZE-1, MSG_OOB );
            printf( "got %d bytes of oob data '%s'
    ", ret, buffer );
    
            memset( buffer, '', BUF_SIZE );
            ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
            printf( "got %d bytes of normal data '%s'
    ", ret, buffer );
    
            close( connfd );
        }
    
        close( sock );
        return 0;
    }
    接收方两种结果:
    [root@vm MOTO]# ./recv 10.8.56.206 12345
    got 5 bytes of normal data '123ab'
    got 1 bytes of oob data 'c'
    got 3 bytes of normal data '123'

    由此可见客户端给服务器的3字节的带外数据“abc"中,仅有最后一个字符"c"被服务器当成了真正的带外数据接收,并且,服务器对正常数据的接收将被带外数据截断,即前一部分正常数据"123ab"和后续的正常数据"123"是不能被一个recv调用全部读取的。

    或者:
    [root@vm MOTO]# ./recv 10.8.56.206 12345
    got 3 bytes of normal data '123'
    got -1 bytes of oob data ''
    got 2 bytes of normal data 'ab'

    客户端发送的"123"到服务器的接收缓冲区,立马被服务器读走,然后客户端发送的带外数据"abc"到服务器的接收缓冲区,当服务器调用recv( connfd, buffer, BUF_SIZE-1, MSG_OOB );时失败,因为第一个字符不是带外数据,而是字符"a",真正的带外数据是"c"。服务器接着读走了"ab"字符后关闭了连接,但是发送的不是结束报文段,发送的是复位报文段,即如果recv( connfd, buffer, BUF_SIZE-1, MSG_OOB );失败时,最后发送的不是结束报文段,是复位报文段。

    接收方结果:
    [root@vm MOTO]# ./send 10.8.56.206 12345
    send oob data out

    在实际应用中,我们通常无法预期带外数据何时到来。好在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种方式是:
    • I/O复用产生的异常事件。
    • SIGURG信号。
    但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这点可通过如下系统调用实现:
           #include <sys/socket.h>
    
           int sockatmark(int fd);
    
    sockatmark判断sockfd是否处于带外标记,即下一个被读到的数据是否是带外数据,如果是,sockatmark返回1,此时我们就是利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则seckatmark返回0。


    用于UDP数据报的读写:
    #include <sys/types.h>
    #include <sys/socket.h>
    ssize_t recvfrom ( int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen );
    ssize_t sendto ( int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen );

    函数说明:
    recvfrom读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小。因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen参数则指定该地址的长度。
    sendto往sockfd上写入数据,buf和len分别指定写缓冲区的位置和大小,dest_addr指定接收端的socket地址,addrlen指定该地址的长度。

    通用数据读写函数:
    #include <sys/socket.h>
    ssize_t recvmsg( int sockfd, struct msghdr* msg, int flags );
    ssize_t sendmsg( int sockfd, struct msghdr* msg, int flags );

    函数说明:
    sockfd:指定被操作的目标sockfd
    msg:msghdr结构体类型的指针,定义如下:
     struct msghdr
    {
        void* msg_name;                    /* socket地址 */
        socklen_t msg_namelen;        /* socktet地址的长度 */
        struct iovec* msg_iov;             /* 分散的内存块 */
        int msg_iovlen;                        /* 分散的内存块数量 */
        void msg_control;                   /* 指向辅助数据的起始位置 */
        socklen_t msg_controllen;      /* 辅助数据的大小 */
        int msg_flags;                          /* 复制函数中的flags参数,并在调用过程中更新 */
    };
    
    struct iovec
    {
        void *iov_base;    /* 内存起始地址 */
        size_t iov_len;       /* 这块内存的长度 */
    };
           由上可见,iovec结构体封装了一块内存的起始位置和长度。msg_iovlen指定这样的iovec结构对象有多少个。对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读;对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写
           recvmsg/sendmsg 的flags参数以及返回值的含义均与send/recv的flags参数及返回值相同。 


    地址信息函数
    功能:获取连接的本端socket地址,以及远端的socket地址。
    函数:
    #include <sys/socket.h>
    int getsockname ( int sockfd, struct sockaddr* address, socklen_t* address_len );    /* 获取sockfd本端socket地址,并将其存储于address参数指定的内存中 */
    int getpeername ( int sockfd, struct sockaddr* address, socklen_t* address_len );    /* 获取sockfd对应的远端socket地址,其参数及返回值的含义与getsockname的参数及返回值相同 */
    getsockname 获取sockfd对应的本端socket地址。
    getpeername 获取sockfd对应的远端socket地址。


    参考资料:
    《Linux高性能服务器编程》
  • 相关阅读:
    《模糊测试--强制发掘安全漏洞的利器》阅读笔记(一)
    BrickerBot
    这些写的很好的PCA文章
    决策树(挖坑待填)
    线性回归
    关于给定DNA序列,如何找到合理的切割位点使得其退火温度保持相对一致
    生成全排列
    AVL树学习笔记
    二叉搜索树
    堆排序
  • 原文地址:https://www.cnblogs.com/wangfengju/p/6172421.html
Copyright © 2011-2022 走看看