Rust 實戰 - 使用套接字聯網API (一)

雖然標準庫已經封裝好了 TcpListenerTcpStream 等基礎api,但做爲Rust 的愛好者,咱們能夠去一探究竟。本文假設你已經對 Rust 和 Linux 操做系統有了必定了解。html

在 Linux 上 Rust 默認會連接的系統的 libc 以及一些其餘的庫,這就意味着,你能夠直接使用libc中的函數。好比,你可使用 gethostname 獲取你電腦的 "hostname":linux

use std::os::raw::c_char;
use std::ffi::CStr;

extern {
    pub fn gethostname(name: *mut c_char, len: usize) -> i32;
}

fn main() {
    let len = 255;
    let mut buf = Vec::<u8>::with_capacity(len);
    let ptr = buf.as_mut_ptr() as *mut c_char;

    unsafe {
        gethostname(ptr, len);
        println!("{:?}", CStr::from_ptr(ptr));
    }
}

解釋一下上面的代碼。編程

extren 表示「外部塊(External blocks)」,用來申明外部非 Rust 庫中的符號。咱們須要使用 Rust 之外的函數,好比 libc ,就須要在 extren 中將須要用到的函數定義出來,而後就能夠像使用本地函數同樣使用外部函數,編譯器會負責幫咱們轉換,是否是很方便呢。可是,調用一個外部函數是unsafe的,編譯器不能提供足夠的保證,因此要放到unsafe塊中。api

若是外部函數有可變參數,能夠這麼申明:數組

extern {
    fn foo(x: i32, ...);
}

不過 Rust 中的函數目前還不支持可變參數。安全

實際上,這裏應該是 extern "C" { .. },由於默認值就是"C",咱們就能夠將其省略。還有一些其餘的可選值,由於這裏不會用到,暫且不討論,你能夠去這兒這兒查看。服務器

再來講說類型。「gethostname」 函數在 C 頭文件中的原型是:網絡

int gethostname(char *name, size_t len);

在 Linux 64位平臺上,C中的int對應於Rust中的intsize_t對應Rust中的usize,但C中的char與Rust中的char是徹底不一樣的,C中的char始終是i8或者u8,而 Rust 中的char是一個unicode標量值。你也能夠去標準庫查看。對於指針,Rust 中的裸指針 與C中的指針幾乎是同樣的,Rust的*mut對應C的普通指針,*const 對應C的const指針。所以咱們將類型一一對應,函數的參數名稱不要求一致。socket

pub fn gethostname(name: *mut i8, len: usize) -> i32;

可是,咱們後面會使用CStr::from_ptr()將C中的字符串轉換爲 Rust 本地字符串,這個函數的定義是:函數

pub unsafe fn from_pt<'a>(ptr: *const c_char) -> &'a CStr

爲了「好看」一點,我就寫成了c_char,可是,c_char只是i8的別名,你寫成i8也沒有問題的。

type c_char = i8;

你能夠看這裏

不過,若是你要是考慮跨平臺的話,可能須要吧 i32 換成 std::os::raw::c_int,並非全部平臺上C中的int都對應Rust中的i32。不過,若是你沒有一一對應類型,必定程度上是可行的,若是沒有發生越界的話。好比像這樣:

use std::os::raw::c_char;
use std::ffi::CStr;

extern {
    pub fn gethostname(name: *mut c_char, len: u16) -> u16;
}

fn main() {
    let len = 255;
    let mut buf = Vec::<u8>::with_capacity(len);
    let ptr = buf.as_mut_ptr() as *mut c_char;

    unsafe {
        gethostname(ptr, len as u16);
        println!("{:?}", CStr::from_ptr(ptr));
    }
}

我把 size_tint 都對應成了 u16,這段代碼是能夠編譯經過,並正確輸出你的hostname的,但我建議,你最好是將類型一一對應上,以減小一些沒必要要的麻煩。固然,你把那個 *mut c_char 換成 *mut i32,也沒問題,反正都是個指針,你能夠試試:

use std::os::raw::c_char;
use std::ffi::CStr;

extern {
    pub fn gethostname(name: *mut i32, len: u16) -> u16;
}

fn main() {
    let len = 255;
    let mut buf = Vec::<u8>::with_capacity(len);
    let ptr = buf.as_mut_ptr() as *mut i32;

    unsafe {
        gethostname(ptr, len as u16);
        println!("{:?}", CStr::from_ptr(ptr as *const i8));
    }
}

你還能夠把 Vec::<u8>換成Vec::<i32> 看看結果。

int gethostname(char *name, size_t len) 這個函數,是接收一個char數組和數組長度,也能夠說成接收緩衝區和接收緩衝區的最大長度。我是建立了一個容量爲255的Vec<u8>,將其可變指針轉換爲裸指針。你也能夠建立能夠長度爲255的u8數組,也沒有問題:

let len = 255;
    let mut buf = [0u8; 255];
    let ptr = buf.as_mut_ptr() as *mut i32;

    unsafe {
        gethostname(ptr, len as u16);
        println!("{:?}", CStr::from_ptr(ptr as *const i8));
    }

爲何這樣能夠,由於Rust的Slice和Vec的底層內存佈局,跟C是同樣的。(注意,Rust中Slice與Array的關係,就像&str與str的關係)。咱們能夠看看Vec和Slice在源碼中的定義:

pub struct Vec<T> {
    buf: RawVec<T>,
    len: usize,
}

pub struct RawVec<T, A: Alloc = Global> {
    ptr: Unique<T>,
    cap: usize,
    a: A,
}

pub struct Unique<T: ?Sized> {
    pointer: *const T,
    _marker: PhantomData<T>,
}

struct FatPtr<T> {
    data: *const T,
    len: usize,
}

Vec是一個結構體,裏面包含buflen兩個字段,len用來表示Vec的長度,buf又指向另外一個結構體RawVec,其中有三個字段,第三個字段a是一個Tarit,不佔內存。cap用來表示Vec的容量,ptr指向另外一個結構體Unique,其中的pointer字段就是一個裸指針了,_marker是給編譯器看的一個標記,也不佔內存,暫時不討論這個,你能夠去看文檔。Slice的結構更簡單,就一個裸指針和長度。

雖然RawVecUnique在標準庫外部是不可見的,但咱們仍是能用必定的「手段」取出裏面值,那就是定義一個內存佈局跟Vec同樣的結構體,「強行」轉換。

#[derive(Debug)]
struct MyVec<T> {
    ptr: *mut T,
    cap: usize,
    len: usize
}

我定義了一個叫作MyVec的結構體,忽略了Vec中兩個不佔用內存的字段,他們的內存佈局是相同的,在64位平臺上都是24(ptr佔8個,另外兩個usize個8個)個字節。你能夠試試:

#[derive(Debug)]
struct MyVec<T> {
    ptr: *mut T,
    cap: usize,
    len: usize
}

println!("{:?}", std::mem::size_of::<Vec<u8>>());
println!("{:?}", std::mem::size_of::<MyVec<u8>>());

我先建立一個Vec<u8>,拿到Vec<u8>的裸指針*const Vec<u8>,再將*const Vec<u8>轉換爲*const MyVec<u8>,以後,解引用,就能獲得MyVec<u8>了。不過,解引裸指針是unsafe的,要謹慎!!! 你還能夠看看標準庫中講述pointer的文檔。

fn main() {
    let vec = Vec::<u8>::with_capacity(255);

    println!("vec ptr: {:?}", vec.as_ptr());

    #[derive(Debug)]
    struct MyVec<T> {
        ptr: *mut T,
        cap: usize,
        len: usize
    }

    let ptr: *const Vec<u8> = &vec;

    let my_vec_ptr: *const MyVec<u8> = ptr as _;

    unsafe {
        println!("{:?}", *my_vec_ptr);
    }
}

而後編譯運行,是否能夠看到相似下面的輸出呢:

vec ptr: 0x557933de6b40
MyVec { ptr: 0x557933de6b40, cap: 255, len: 0 }

你能夠看到,咱們調用vec.as_ptr()獲得的就是Vec內部的那個裸指針。

對於std::mem::size_of 相等的兩個類型,你也可使用std::mem::transmute 這個函數轉換,跟上面的經過裸指針間接轉換,幾乎是等效的,只是會多加一個驗證,若是兩個類型size_of不相等的話,是沒法經過編譯的。這個函數是unsafe的。

你還能夠繼續嘗試,好比把Vec<u8>轉換爲長度爲3(或者更小更大)的usize數組,像是這樣:

fn main() {
    let vec = Vec::<u8>::with_capacity(255);

    println!("vec ptr: {:?}", vec.as_ptr());

    let ptr: *const Vec<u8> = &vec;

    unsafe {
        let aaa_ptr: *const [usize; 2] = ptr as _;
        println!("{:?}", (*aaa_ptr)[0] as *const u8);
    }
}

不過,因爲Rust中Vec的擴容機制,這段代碼是存在必定問題的:

fn main() {
    let len = 255;
    let mut buf = Vec::<u8>::with_capacity(len);
    let ptr = buf.as_mut_ptr() as *mut c_char;

    unsafe {
        gethostname(ptr, len);
        println!("{:?}", CStr::from_ptr(ptr));
    }

    println!("{:?}", buf);
}

雖然獲取到了正確的主機名,可是以後你打印buf會發現,buf是空的,這個問題留給你去探究。

你已經看到,Rust已經變得「不安全」,這又不當心又引入了另外一個話題--《 Meet Safe and Unsafe》。不過,仍是儘快迴歸正題,等以後有機會再說這個話題。

提及套接字API,主要包括TCP、UDP、SCTP相關的函數,I/O複用函數和高級I/O函數。其中大部分函數Rust標準裏是沒有的,若是標準庫不能知足你的需求,你能夠直接調用libc中的函數。實際上,標準庫中,網絡這一塊,也基本是對libc中相關函數的封裝。

先從TCP開始。TCP套接字編程主要會涉及到socketconnectbindlistenacceptclosegetsocknamegetpeername等函數。先來看看這些函數的定義:

// socket 函數用來指按期望的通訊協議類型,並返回套接字描述符
int socket(int family, int type, int protocol); // 成功返回監聽描述符。用來設置監聽,出錯爲-1
// family是表示socket使用的協議類型,對於TCP,一般設置爲 `AF_INET` 或`AF_INET6`,表示`IPv4`和`IPv6`
// type是建立的套接字類型,TCP是字節流套接字,因此這裏設置爲`SOCK_STREAM`,可選的值還有
// `SOCK_DGRAM`用於UDP,`SOCK_SEQPACKET`用於SCTP
// protocol協議的標識,能夠設置爲0,讓系統選擇默認值。可選的值有`IPPROTO_TCP`、`IPPROTO_UDP`、`IPPROTO_SCTP`

// connect 函數被客戶端用來聯立與TCP服務器的鏈接
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); // 成功返回0 ,出錯爲-1
// sockfd 是由 socket 函數返回的套接字描述符,第二和第三個參數分別指向一個指向套接字地址結構的指針和該指針的長度

// bind 函數把一個本地協議地址賦予一個套接字。
int bind(int sockfd, const struct sockaddr *myaddr,  socklen_t addrlen); // 成功返回0 ,出錯爲-1
// 第二個和第三個參數分別是指向特色於協議的地址結構的指針和指針的長度

// listen 函數把一個未鏈接的套接字轉換成一個被動套接字,指示內核應接受指向該套接字的鏈接請求。
int listen(int sockfd, int backlog); // 成功返回0 ,出錯爲-1
// 第二個參數指定內核該爲相應套接字排隊的最大鏈接個數。

// accept 函數由TCP服務器調用,用於從已完成鏈接的隊列頭返回下一個已完成的鏈接。
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); // 成功返回非負描述符,錯誤返回-1
// 第二個和第三個參數用來返回客戶端的協議地址和該地址的大小

// close 用來關閉套接字,並終止TCP鏈接
int close(int sockfd); // 成功返回0 ,出錯爲-1

// getsockname 和 getpeername 函數返回與某個套接字關聯的本地協議地址和外地協議地址
int getsockname(int sockfd,struct sockaddr *localaddr,socklen_t *addrlen); // 成功返回0 ,出錯爲-1
int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addelen); // 成功返回0 ,出錯爲-1

還有一對常見的函數,readwrite 用於讀寫數據。另外還有三對高級I/O函數,recv/sendreadv/writevrecvmsg/sendmsg等須要的時候再加。

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

除了函數外,還有幾個常量和sockaddr這個結構體。常量咱們須要在Rust這邊定義出來,只定義出須要的:

const AF_INET: i32 = 2;
const AF_INET6: i32 = 10;
const SOCK_STREAM: i32 = 1;
const IPPROTO_TCP: i32 = 6;

除了sockaddr外,還有幾個與之相關的結構體,他們在C中的定是:

struct sockaddr
{
    unsigned short    int sa_family; // 地址族
    unsigned char     sa_data[14];  // 包含套接字中的目標地址和端口信息
};

struct sockaddr_in
{
    sa_family_t       sin_family;
    uint16_t          sin_port;
    struct in_addr    sin addr;
    char              sin_zero[8];
};

struct in_addr
{
    In_addr_t  s_addr;
};

struct sockaddr_in6
{
    sa_family_t       sin_family;
    in_port_t         sin6_port;
    uint32_t          sin6_flowinfo;
    struct in6_addr   sin6_addr; 
    uint32_t          sin6_scope_id;
};

struct in6_addr
{
    uint8_t           s6_addr[16]
};

struct sockaddr_storage {
    sa_family_t       ss_family;     // address family

    // all this is padding, implementation specific, ignore it:
    char              __ss_pad1[_SS_PAD1SIZE];
    int64_t           __ss_align;
    char              __ss_pad2[_SS_PAD2SIZE];
};

而後,須要在Rust中定義出佈局相同的結構體:

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct sockaddr {
    pub sa_family: u16,
    pub sa_data: [c_char; 14],
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct sockaddr_in {
    pub sin_family: u16,
    pub sin_port: u16,
    pub sin_addr: in_addr,
    pub sin_zero: [u8; 8],
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct in_addr {
    pub s_addr: u32,
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct sockaddr_in6 {
    pub sin6_family: u16,
    pub sin6_port: u16,
    pub sin6_flowinfo: u32,
    pub sin6_addr: in6_addr,
    pub sin6_scope_id: u32,
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct in6_addr {
    pub s6_addr: [u8; 16],
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct sockaddr_storage {
    pub ss_family: u16,
    _unused: [u8; 126]
}

你須要在結構體前面加一個#[repr(C)]標籤,以確保結構體的內存佈局跟C一致,由於,Rust結構體的內存對齊規則,可能跟C是不同的。#[derive(Debug, Clone, Copy)] 不是必須的。對於最後一個結構體sockaddr_storage,我也很迷,我不知道在Rust中如何定義出來,可是我知道它佔128個字節,而後我就定義一個長度爲126的u8數組,湊夠128位。

接下來,繼續把那幾個函數定義出來:

extern {
    pub fn socket(fanily: i32, ty: i32, protocol: i32) -> i32;
    pub fn connect(sockfd: i32, servaddr: *const sockaddr, addrlen: u32) -> i32;
    pub fn bind(sockfd: i32, myaddr: *const sockaddr, addrlen: u32) -> i32;
    pub fn listen(sockfd: i32, backlog: i32);
    pub fn accept(sockfd: i32, cliaddr: *mut sockaddr, addrlen: u32) -> i32;
    pub fn close(sockfd: i32) -> i32;
    pub fn getsockname(sockfd: i32, localaddr: *mut sockaddr, addrlen: *mut u32) -> i32;
    pub fn getpeername(sockfd: i32, peeraddr: *mut sockaddr, addrlen: *mut u32) -> i32;
    pub fn read(fd: i32, buf: *mut std::ffi::c_void, count: usize) -> isize;
    pub fn write(fd: i32, buf: *const std::ffi::c_void, count: usize) -> isize;
}

對於readwrite 裏的參數buf類型void, 可使用標準庫提供的 std::ffi::c_void,也能夠是*mut u8/*const u8,像是下面這樣:

pub fn read(fd: i32, buf: *mut u8, count: usize) -> isize;
pub fn write(fd: i32, buf: *const u8, count: usize) -> isize;

或者,既然void自己是個「動態類型」,也能夠傳個其餘類型的指針進去的,以後你能夠試試,不過可能會有點危險。

看看目前的代碼:

use std::os::raw::c_char;
use std::ffi::c_void;

pub const AF_INET: i32 = 2;
pub const AF_INET6: i32 = 10;
pub const SOCK_STREAM: i32 = 1;
pub const IPPRPTO_TCP: i32 = 6;

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct sockaddr {
    pub sa_family: u16,
    pub sa_data: [c_char; 14],
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct sockaddr_in {
    pub sin_family: u16,
    pub sin_port: u16,
    pub sin_addr: in_addr,
    pub sin_zero: [u8; 8],
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct in_addr {
    pub s_addr: u32,
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct sockaddr_in6 {
    pub sin6_family: u16,
    pub sin6_port: u16,
    pub sin6_flowinfo: u32,
    pub sin6_addr: in6_addr,
    pub sin6_scope_id: u32,
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct in6_addr {
    pub s6_addr: [u8; 16],
}

#[repr(C)]
#[derive(Clone, Copy)]
pub struct sockaddr_storage {
    pub ss_family: u16,
    _unused: [u8; 126]
}

extern {
    pub fn socket(fanily: i32, ty: i32, protocol: i32) -> i32;
    pub fn connect(sockfd: i32, servaddr: *const sockaddr, addrlen: u32) -> i32;
    pub fn bind(sockfd: i32, myaddr: *const sockaddr, addrlen: u32) -> i32;
    pub fn listen(sockfd: i32, backlog: i32);
    pub fn accept(sockfd: i32, cliaddr: *mut sockaddr, addrlen: *mut u32) -> i32;
    pub fn close(sockfd: i32) -> i32;
    pub fn getsockname(sockfd: i32, localaddr: *mut sockaddr, addrlen: *mut u32) -> i32;
    pub fn getpeername(sockfd: i32, peeraddr: *mut sockaddr, addrlen: *mut u32) -> i32;
    pub fn read(fd: i32, buf: *mut std::ffi::c_void, count: usize) -> isize;
    pub fn write(fd: i32, buf: *const std::ffi::c_void, count: usize) -> isize;
}

而後,咱們能夠寫一個簡單的服務器和客戶端程序:服務器監聽一個地址,客戶端鏈接服務器,而後向服務器發送「Hello, server!」,服務器迴應「Hi,client!」,客戶端收到後斷開鏈接。

fn main() {
    use std::io::Error;
    use std::mem;
    use std::thread;
    use std::time::Duration;

    thread::spawn(|| {

        // server
        unsafe {
            let socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
            if socket < 0 {
                panic!("last OS error: {:?}", Error::last_os_error());
            }

            let servaddr = sockaddr_in {
                sin_family: AF_INET as u16,
                sin_port: 8080u16.to_be(),
                sin_addr: in_addr {
                    s_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be()
                },
                sin_zero: mem::zeroed()
            };

            let result = bind(socket, &servaddr as *const sockaddr_in as *const sockaddr, mem::size_of_val(&servaddr) as u32);
            if result < 0 {
                println!("last OS error: {:?}", Error::last_os_error());
                close(socket);
            }

            listen(socket, 128);

            loop {
                let mut cliaddr: sockaddr_storage = mem::zeroed();
                let mut len = mem::size_of_val(&cliaddr) as u32;

                let client_socket = accept(socket, &mut cliaddr as *mut sockaddr_storage as *mut sockaddr, &mut len);
                if client_socket < 0 {
                    println!("last OS error: {:?}", Error::last_os_error());
                    break;
                }

                thread::spawn(move || {
                    loop {
                        let mut buf = [0u8; 64];
                        let n = read(client_socket, &mut buf as *mut _ as *mut c_void, buf.len());
                        if n <= 0 {
                            break;
                        }

                        println!("{:?}", String::from_utf8_lossy(&buf[0..n as usize]));

                        let msg = b"Hi, client!";
                        let n = write(client_socket, msg as *const _ as *const c_void, msg.len());
                        if n <= 0 {
                            break;
                        }
                    }

                    close(client_socket);
                });
            }

            close(socket);
        }

    });

    thread::sleep(Duration::from_secs(1));

    // client
    unsafe {
        let socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if socket < 0 {
            panic!("last OS error: {:?}", Error::last_os_error());
        }

        let servaddr = sockaddr_in {
            sin_family: AF_INET as u16,
            sin_port: 8080u16.to_be(),
            sin_addr: in_addr {
                s_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be()
            },
            sin_zero: mem::zeroed()
        };

        let result = connect(socket, &servaddr as *const sockaddr_in as *const sockaddr, mem::size_of_val(&servaddr) as u32);
        if result < 0 {
            println!("last OS error: {:?}", Error::last_os_error());
            close(socket);
        }

        let msg = b"Hello, server!";
        let n = write(socket, msg as *const _ as *const c_void, msg.len());
        if n <= 0 {
            println!("last OS error: {:?}", Error::last_os_error());
            close(socket);
        }

        let mut buf = [0u8; 64];
        let n = read(socket, &mut buf as *mut _ as *mut c_void, buf.len());
        if n <= 0 {
            println!("last OS error: {:?}", Error::last_os_error());
        }

        println!("{:?}", String::from_utf8_lossy(&buf[0..n as usize]));

        close(socket);
    }
}

調用外部函數是unsafe的,我爲了簡單省事,暫時把代碼放到了一個大的unsafe {} 中,以後咱們再把他們封裝成safe的API。爲了方便測試,我把服務器程序放到了一個線程裏,而後等待1秒後,再讓客戶端創建鏈接。

std::io::Error::last_os_error 這個函數,是用來捕獲函數操做失敗後,內核反饋給咱們的錯誤。

在調用bindconnect 函數時,先要建立sockaddr_in結構體,端口(sin_port)和IP地址(s_addr) 是網絡字節序(big endian),因而我調用了u16u32to_be()方法將其轉換爲網絡字節序。u32::from_be_bytes 函數是將[127u8, 0u8, 0u8, 1u8] 轉換爲u32整數,因爲咱們看到的已是大端了,轉換回去會變成小端,因而後面又調用了to_be(),你也能夠直接u32::from_le_bytes([127, 0, 0, 1])。而後使用了std::mem::zeroed 函數建立一個[0u8; 8] 數組,你也能夠直接[0u8; 8],在這裏他們是等效的。接着,咱們進行強制類型轉換,將&sockaddr_in 轉換爲*const sockaddr_in類型,又繼續轉換爲*const sockaddr,若是你理解了一開始「gethostname」那個例子話,這裏應該很好理解。這裏還能夠簡寫成&servaddr as *const _ as *const _,編譯器會自動推導類型。

在調用accept函數時,先建立了一個mut sockaddr_storage,一樣進行類型轉換。之因此用sockaddr_storage 而不是sockaddr_insockaddr_in6是由於sockaddr_storage這個通用結構足夠大,能承載sockaddr_insockaddr_in6等任何套接字的地址結構,所以,咱們若是把套接bind到一個IPv6地址上的話,這裏的代碼是不須要修改的。我仍是用std::mem::zeroed 函數初始化sockaddr_storage,它的結構我也很迷惑,因此就藉助了這個函數,這個函數是unsafe的,使用的時候要當心。你也能夠繼續嘗試這個函數:

let mut a: Vec<u8> = unsafe { std::mem::zeroed() };
a.push(123);
println!("{:?}", a);

readwrite 時,一樣要類型轉換。

不少時候,類型根本「強不起來」。OK,這一節的內容就先到這裏。

相關文章
相關標籤/搜索