上一節(jié)签餐,我們已經(jīng)實(shí)現(xiàn)了一個(gè)最小可運(yùn)行版本。之所以使用Rust而不是C愚战,是因?yàn)镽ust具備了必要的抽象能力娇唯,還能獲得跟C差不多的性能。這一節(jié)寂玲,我們對(duì)上一節(jié)的代碼做必要的封裝塔插,順便還能把unsafe
的代碼包裝成safe
的API。
我將上一節(jié)的源碼放到了這里拓哟,你可以去查看想许。
還記得上一節(jié),我們把使用到的libc
中的函數(shù)socket
断序、bind
流纹、connect
和結(jié)構(gòu)體sockaddr
、sockaddr_in
违诗、in_addr
等漱凝,在Rust這邊定義了出來(lái)。實(shí)際上诸迟,幾乎libc
中的函數(shù)茸炒,libc這個(gè)crate都幫我們定義好了愕乎。你可以去這里查看。編譯器和標(biāo)準(zhǔn)庫(kù)本身也使用了這個(gè)crate壁公,我們也使用這個(gè)感论。
首先在Cargo.toml
文件的[dependencies]
下面加入libc = "0.2"
:
[dependencies]
libc = "0.2"
接著在main.rs
文件上方加入use libc;
,也可以use libc as c;
紊册”纫蓿或者你直接簡(jiǎn)單粗暴use libc::*
,并不推薦這樣湿硝,除非你明確知道你使用的函數(shù)來(lái)自哪里薪前。并將我們定義的與libc
中對(duì)用的常量、函數(shù)关斜、結(jié)構(gòu)體刪除示括。再添加libc::
或c::
到我們使用那些常量、結(jié)構(gòu)體痢畜、函數(shù)的地方垛膝。如果你是直接use libc::*
,除了直接刪除那部分代碼外丁稀,幾乎什么都不用做吼拥。目前的代碼:
use std::ffi::c_void;
use libc as c;
fn main() {
use std::io::Error;
use std::mem;
use std::thread;
use std::time::Duration;
thread::spawn(|| {
// server
unsafe {
let socket = c::socket(c::AF_INET, c::SOCK_STREAM, c::IPPROTO_TCP);
if socket < 0 {
panic!("last OS error: {:?}", Error::last_os_error());
}
let servaddr = c::sockaddr_in {
sin_family: c::AF_INET as u16,
sin_port: 8080u16.to_be(),
sin_addr: c::in_addr {
s_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be()
},
sin_zero: mem::zeroed()
};
let result = c::bind(socket, &servaddr as *const c::sockaddr_in as *const c::sockaddr, mem::size_of_val(&servaddr) as u32);
if result < 0 {
println!("last OS error: {:?}", Error::last_os_error());
c::close(socket);
}
c::listen(socket, 128);
loop {
let mut cliaddr: c::sockaddr_storage = mem::zeroed();
let mut len = mem::size_of_val(&cliaddr) as u32;
let client_socket = c::accept(socket, &mut cliaddr as *mut c::sockaddr_storage as *mut c::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 = c::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 = c::write(client_socket, msg as *const _ as *const c_void, msg.len());
if n <= 0 {
break;
}
}
c::close(client_socket);
});
}
c::close(socket);
}
});
thread::sleep(Duration::from_secs(1));
// client
unsafe {
let socket = c::socket(c::AF_INET, c::SOCK_STREAM, c::IPPROTO_TCP);
if socket < 0 {
panic!("last OS error: {:?}", Error::last_os_error());
}
let servaddr = c::sockaddr_in {
sin_family: c::AF_INET as u16,
sin_port: 8080u16.to_be(),
sin_addr: c::in_addr {
s_addr: u32::from_be_bytes([127, 0, 0, 1]).to_be()
},
sin_zero: mem::zeroed()
};
let result = c::connect(socket, &servaddr as *const c::sockaddr_in as *const c::sockaddr, mem::size_of_val(&servaddr) as u32);
if result < 0 {
println!("last OS error: {:?}", Error::last_os_error());
c::close(socket);
}
let msg = b"Hello, server!";
let n = c::write(socket, msg as *const _ as *const c_void, msg.len());
if n <= 0 {
println!("last OS error: {:?}", Error::last_os_error());
c::close(socket);
}
let mut buf = [0u8; 64];
let n = c::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]));
c::close(socket);
}
}
你編譯運(yùn)行,應(yīng)該能得到與上一節(jié)同樣的結(jié)果线衫。
接下來(lái)凿可,我們嘗試把上面代碼中函數(shù),封裝成更具Rust風(fēng)格的API授账,除了TCP外枯跑,也還要考慮之后把UDP、UNIX域和SCTP也增加進(jìn)來(lái)白热。同時(shí)敛助,我們跟標(biāo)準(zhǔn)庫(kù)里 net
相關(guān)的API保持一致的風(fēng)格。我們暫時(shí)不考慮跨平臺(tái)屋确,只考慮Linux纳击,因此可以大膽的將一些linux獨(dú)有的API添加進(jìn)來(lái)。
UNIX中一切皆文件攻臀,套接字也不例外焕数。字節(jié)流套接字上的read和write函數(shù)所表現(xiàn)出來(lái)的行為,不同于通常的文件I/O茵烈。字節(jié)流套接字上調(diào)用read和write輸入或輸出字節(jié)數(shù)可能比請(qǐng)求的要少百匆,這個(gè)現(xiàn)象的原因在于內(nèi)核中用于套接字的緩沖區(qū)可能已經(jīng)達(dá)到了極限。不過(guò)呜投,這并不是我們正真關(guān)心的加匈。我們來(lái)看看標(biāo)準(zhǔn)庫(kù)中 File的實(shí)現(xiàn):
pub struct File(FileDesc);
impl File {
...
pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
pub fn duplicate(&self) -> io::Result<File> {
self.0.duplicate().map(File)
}
...
}
File
是一個(gè)元組結(jié)構(gòu)體,標(biāo)準(zhǔn)庫(kù)已經(jīng)實(shí)現(xiàn)了read
和write
仑荐,以及duplicate
雕拼。duplicate
很有用,用于復(fù)制出一個(gè)新的描述符粘招。我們繼續(xù)看File
中"包裹的FileDesc:
pub struct FileDesc {
fd: c_int,
}
impl File {
...
pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
let ret = cvt(unsafe {
libc::read(self.fd,
buf.as_mut_ptr() as *mut c_void,
cmp::min(buf.len(), max_len()))
})?;
Ok(ret as usize)
}
pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
let ret = cvt(unsafe {
libc::write(self.fd,
buf.as_ptr() as *const c_void,
cmp::min(buf.len(), max_len()))
})?;
Ok(ret as usize)
}
pub fn set_cloexec(&self) -> io::Result<()> {
unsafe {
cvt(libc::ioctl(self.fd, libc::FIOCLEX))?;
Ok(())
}
}
pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
unsafe {
let v = nonblocking as c_int;
cvt(libc::ioctl(self.fd, libc::FIONBIO, &v))?;
Ok(())
}
}
}
這一層應(yīng)該是到頭了啥寇,你可以看到,Rust中的File
也是直接對(duì)libc
的封裝洒扎,不過(guò)你不用擔(dān)心辑甜,一開(kāi)始就提到,Rust 的ABI與C的ABI是兼容的袍冷,也就意味著Rust和C互相調(diào)用是幾乎是零開(kāi)銷的磷醋。FileDesc
的read
和write
中的實(shí)現(xiàn),與我們之前對(duì)sockfd
的read
和write
基本是一樣的胡诗。除了read
和write
外邓线,還有兩個(gè)很有用的方法set_cloexec
和set_nonblocking
。
我把“依附于”某個(gè)類型的函數(shù)叫做方法煌恢,與普通函數(shù)不同的是骇陈,依附于某個(gè)類型的函數(shù),必須通過(guò)它所依附的類型調(diào)用瑰抵。Rust通過(guò)這種方式來(lái)實(shí)現(xiàn)OOP你雌,但是與某些語(yǔ)言的OOP不同的是,Rust的這種實(shí)現(xiàn)是零開(kāi)銷的二汛。也就是婿崭,你將一些函數(shù)依附到某個(gè)類型上,并不會(huì)對(duì)運(yùn)行時(shí)造成額外的開(kāi)銷习贫,這些都在編譯時(shí)去處理逛球。
set_cloexec
方法會(huì)對(duì)描述符設(shè)置FD_CLOEXEC
。我們經(jīng)常會(huì)碰到需要fork子進(jìn)程的情況苫昌,而且子進(jìn)程很可能會(huì)繼續(xù)exec新的程序颤绕。對(duì)描述符設(shè)置FD_CLOEXEC
,就意味著祟身,我們fork子進(jìn)程時(shí)奥务,父子進(jìn)程中相同的文件描述符指向系統(tǒng)文件表的同一項(xiàng),但是袜硫,我們?nèi)绻{(diào)用exec執(zhí)行另一個(gè)程序氯葬,此時(shí)會(huì)用全新的程序替換子進(jìn)程的正文。為了較少不必要的麻煩婉陷,我們以后要對(duì)打開(kāi)的描述符設(shè)置FD_CLOEXEC
帚称,除非遇到特殊情況官研。
set_nonblocking
用于將描述符設(shè)置為非阻塞模式,如果我們要使用poll闯睹、epoll等api的話戏羽。
既然標(biāo)準(zhǔn)庫(kù)已經(jīng)封裝好了FileDesc
,我想直接使用的楼吃,然而FileDesc
在標(biāo)準(zhǔn)庫(kù)之外是不可見(jiàn)的始花。如果使用File
的話,set_cloexec
和 set_nonblocking
還是要我們?cè)賹懸淮魏⑽?code>File并不是“我自己”的類型酷宵,我沒(méi)法直接給File
附加方法,為此還需要一個(gè)額外的Tarit或者用一個(gè)“我自己”的類型躬窜,去包裹它浇垦。挺繞的。那既然這樣斩披,我們還是自己來(lái)吧溜族。不過(guò)我們已經(jīng)有了參考,可以將標(biāo)準(zhǔn)庫(kù)里的FileDecs
直接復(fù)制出來(lái)垦沉,然后去掉與Linux無(wú)關(guān)的代碼煌抒,當(dāng)然你也可以自由發(fā)揮一下。
要注意的是厕倍,這段代碼中還調(diào)用了一個(gè)函數(shù)cvt寡壮,我們把相關(guān)代碼也復(fù)制過(guò)來(lái):
use std::io::{self, ErrorKind};
#[doc(hidden)]
pub trait IsMinusOne {
fn is_minus_one(&self) -> bool;
}
macro_rules! impl_is_minus_one {
($($t:ident)*) => ($(impl IsMinusOne for $t {
fn is_minus_one(&self) -> bool {
*self == -1
}
})*)
}
impl_is_minus_one! { i8 i16 i32 i64 isize }
pub fn cvt<T: IsMinusOne>(t: T) -> io::Result<T> {
if t.is_minus_one() {
Err(io::Error::last_os_error())
} else {
Ok(t)
}
}
pub fn cvt_r<T, F>(mut f: F) -> io::Result<T>
where T: IsMinusOne,
F: FnMut() -> T
{
loop {
match cvt(f()) {
Err(ref e) if e.kind() == ErrorKind::Interrupted => {}
other => return other,
}
}
}
還記得上一節(jié)我們使用過(guò)的last_os_error()
方法么,這段代碼通過(guò)宏impl_is_minus_one
為 i32
等常見(jiàn)類型實(shí)現(xiàn)了IsMinusOne
這個(gè)Tarit
讹弯,然后我們就可以使用cvt
函數(shù)更便捷得調(diào)用last_os_error()
取得錯(cuò)誤况既。 我將這段代碼放到util.rs
文件中,并在main.rs
文件上方加入pub mod util;
然后再來(lái)看FileDesc
最終的實(shí)現(xiàn):
use std::mem;
use std::io;
use std::cmp;
use std::os::unix::io::FromRawFd;
use libc as c;
use crate::util::cvt;
#[derive(Debug)]
pub struct FileDesc(c::c_int);
pub fn max_len() -> usize {
<c::ssize_t>::max_value() as usize
}
impl FileDesc {
pub fn raw(&self) -> c::c_int {
self.0
}
pub fn into_raw(self) -> c::c_int {
let fd = self.0;
mem::forget(self);
fd
}
pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
let ret = cvt(unsafe {
c::read(
self.0,
buf.as_mut_ptr() as *mut c::c_void,
cmp::min(buf.len(), max_len())
)
})?;
Ok(ret as usize)
}
pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
let ret = cvt(unsafe {
c::write(
self.0,
buf.as_ptr() as *const c::c_void,
cmp::min(buf.len(), max_len())
)
})?;
Ok(ret as usize)
}
pub fn get_cloexec(&self) -> io::Result<bool> {
unsafe {
Ok((cvt(libc::fcntl(self.0, c::F_GETFD))? & libc::FD_CLOEXEC) != 0)
}
}
pub fn set_cloexec(&self) -> io::Result<()> {
unsafe {
cvt(c::ioctl(self.0, c::FIOCLEX))?;
Ok(())
}
}
pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
unsafe {
let v = nonblocking as c::c_int;
cvt(c::ioctl(self.0, c::FIONBIO, &v))?;
Ok(())
}
}
pub fn duplicate(&self) -> io::Result<FileDesc> {
cvt(unsafe { c::fcntl(self.0, c::F_DUPFD_CLOEXEC, 0) }).and_then(|fd| {
let fd = FileDesc(fd);
Ok(fd)
})
}
}
impl FromRawFd for FileDesc {
unsafe fn from_raw_fd(fd: c::c_int) -> FileDesc {
FileDesc(fd)
}
}
impl Drop for FileDesc {
fn drop(&mut self) {
let _ = unsafe { c::close(self.0) };
}
}
我已經(jīng)將與Linux
不相關(guān)的代碼刪除掉了组民。之所以原有duplicate
那么冗長(zhǎng)棒仍,是因?yàn)榕f的Linux內(nèi)核不支持F_DUPFD_CLOEXEC
這個(gè)設(shè)置。fcntl
這個(gè)函數(shù)臭胜,用來(lái)設(shè)置控制文件描述符的選項(xiàng),我們稍后還會(huì)遇到用來(lái)設(shè)置和獲取套接字的getsockopt
和setsockopt
。還有read_at
和write_at
等實(shí)現(xiàn)比較復(fù)雜的函數(shù),我們用不到,也將他們刪除淫茵。還有impl<'a> Read for &'a FileDesc
爪瓜,因?yàn)閮?nèi)部使了一個(gè)Unstable的API匙瘪,我也將其去掉了。
我自由發(fā)揮了一下蝶缀,把:
pub struct FileDesc {
fd: c_int,
}
替換成了:
pub struct FileDesc(c::c_int);
它們是等效的鳍悠。不知你注意到?jīng)]有,我把pub fn new(...)
函數(shù)給去掉了涧卵,因?yàn)檫@個(gè)函數(shù)是unsafe
的----如果我們今后將這些代碼作為庫(kù)讓別人使用的話伐脖,他可能傳入了一個(gè)不存在的描述符巫俺,并由此可能引起程序崩潰----但他們并不一定知道撼港。我們可以通過(guò)在這個(gè)函數(shù)前面加unsafe
來(lái)告訴使用者這個(gè)函數(shù)是unsafe
的: pub unsafe fn new(...)
。不過(guò),Rust的開(kāi)發(fā)者們已經(jīng)考慮到了這一點(diǎn)赫段,我們用約定俗成的from_raw_fd
來(lái)代替pub unsafe fn new(...)
,于是才有了下面這一段:
impl FromRawFd for FileDesc {
unsafe fn from_raw_fd(fd: c::c_int) -> FileDesc {
FileDesc(fd)
}
}
最后撩银,還利用Rust的drop
實(shí)現(xiàn)了close
函數(shù)给涕,也就意味著,描述符離開(kāi)作用域后,會(huì)自動(dòng)close
够庙,就不再需要我們手動(dòng)close
了恭应。與之先關(guān)的是into_raw
方法,意思是把FileDesc
轉(zhuǎn)換為“未加工的”或者說(shuō)是“裸的”描述符耘眨,也就是C的描述符昼榛。這個(gè)方法里面調(diào)用了forget,之后變量離開(kāi)作用域后剔难,就不會(huì)調(diào)用drop
了胆屿。當(dāng)你使用這個(gè)方法拿到描述符,使用完請(qǐng)不要忘記手動(dòng)close
或者再次from_raw_fd
钥飞。
pub fn into_raw(self) -> c::c_int {
let fd = self.0;
mem::forget(self);
fd
}
我將這段代碼放到了一個(gè)新的文件fd.rs
中莺掠,并在main.rs
文件上方加入pub mod fd;
。
接著读宙,我們還需一個(gè)Socket
類型,將socket
楔绞、bind
结闸、connect
等函數(shù)附加上去。這一步應(yīng)該簡(jiǎn)單多了酒朵。同時(shí)你也會(huì)發(fā)現(xiàn)桦锄,我們已經(jīng)把unsafe
的代碼,封裝成了safe
的代碼蔫耽。
use std::io;
use std::mem;
use std::os::unix::io::{RawFd, AsRawFd, FromRawFd};
use libc as c;
use crate::fd::FileDesc;
use crate::util::cvt;
pub struct Socket(FileDesc);
impl Socket {
pub fn new(family: c::c_int, ty: c::c_int, protocol: c::c_int) -> io::Result<Socket> {
unsafe {
cvt(c::socket(family, ty | c::SOCK_CLOEXEC, protocol))
.map(|fd| Socket(FileDesc::from_raw_fd(fd)))
}
}
pub fn bind(&self, storage: *const c::sockaddr, len: c::socklen_t) -> io::Result<()> {
self.setsockopt(c::SOL_SOCKET, c::SO_REUSEADDR, 1)?;
cvt(unsafe { c::bind(self.0.raw(), storage, len) })?;
Ok(())
}
pub fn listen(&self, backlog: c::c_int) -> io::Result<()> {
cvt(unsafe { c::listen(self.0.raw(), backlog) })?;
Ok(())
}
pub fn accept(&self, storage: *mut c::sockaddr, len: *mut c::socklen_t) -> io::Result<Socket> {
let fd = cvt(unsafe { c::accept4(self.0.raw(), storage, len, c::SOCK_CLOEXEC) })?;
Ok(Socket(unsafe { FileDesc::from_raw_fd(fd) }))
}
pub fn connect(&self, storage: *const c::sockaddr, len: c::socklen_t) -> io::Result<()> {
cvt(unsafe { c::connect(self.0.raw(), storage, len) })?;
Ok(())
}
pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
self.0.set_nonblocking(nonblocking)
}
pub fn get_cloexec(&self) -> io::Result<bool> {
self.0.get_cloexec()
}
pub fn set_cloexec(&self) -> io::Result<()> {
self.0.set_cloexec()
}
pub fn setsockopt<T>(&self, opt: libc::c_int, val: libc::c_int, payload: T) -> io::Result<()> {
unsafe {
let payload = &payload as *const T as *const libc::c_void;
cvt(libc::setsockopt(
self.0.raw(),
opt,
val,
payload,
mem::size_of::<T>() as libc::socklen_t
))?;
Ok(())
}
}
pub fn getsockopt<T: Copy>(&self, opt: libc::c_int, val: libc::c_int) -> io::Result<T> {
unsafe {
let mut slot: T = mem::zeroed();
let mut len = mem::size_of::<T>() as libc::socklen_t;
cvt(libc::getsockopt(
self.0.raw(),
opt,
val,
&mut slot as *mut T as *mut libc::c_void,
&mut len
))?;
assert_eq!(len as usize, mem::size_of::<T>());
Ok(slot)
}
}
}
impl FromRawFd for Socket {
unsafe fn from_raw_fd(fd: RawFd) -> Socket {
Socket(FileDesc::from_raw_fd(fd))
}
}
impl AsRawFd for Socket {
fn as_raw_fd(&self) -> RawFd {
self.0.raw()
}
}
我已經(jīng)將上一節(jié)中我們使用到的socket相關(guān)的主要的5個(gè)函數(shù)结耀,外加read
,write
匙铡,等幾個(gè)描述符設(shè)置的函數(shù)图甜,“依附”到了Socket
上。保存在 socket.rs
文件里鳖眼。
要說(shuō)明的是黑毅,我在new
和accept
方法中,通過(guò)flags
直接為新創(chuàng)建的描述符設(shè)置了SOCK_CLOEXEC
選項(xiàng)钦讳,如果不想一步設(shè)置的話矿瘦,就需要?jiǎng)?chuàng)建出描述符后,再調(diào)用set_cloexec
方法愿卒。bind
中缚去,在調(diào)用c::bind
之前,我給套接字設(shè)置了個(gè)選項(xiàng)SO_REUSEADDR
琼开,意為允許重用本地地址易结,這里不展開(kāi)講,如果你細(xì)心的話就會(huì)發(fā)現(xiàn),上一節(jié)的例子衬衬,如果沒(méi)有正常關(guān)閉socket的話买猖,就可能會(huì)出現(xiàn)error:98,Address already in use
滋尉,等一會(huì)兒才會(huì)好玉控。accept4
不是個(gè)標(biāo)準(zhǔn)的方法,只有Linux才支持狮惜,我們暫時(shí)不考慮兼容性高诺。setsockopt
和getsockopt
方法中涉及到了類型轉(zhuǎn)換,結(jié)合前面的例子碾篡,這里應(yīng)該難不倒你了虱而。除了from_raw_fd
,我還又給Socket
實(shí)現(xiàn)了又一個(gè)約定俗成的方法as_raw_fd
开泽。
我已經(jīng)將遠(yuǎn)嗎放到了這里牡拇,你可以去查看。你還可以嘗試將上一節(jié)的例子穆律,修改成我們今天封裝的Socket
惠呼。這一節(jié)到這里就結(jié)束了。