Rust practice – using socket networking API (II)

Time:2021-9-21

In the previous section, we have implemented a minimum runnable version. The reason for using rust instead of C is that rust has the necessary abstraction ability and can achieve the same performance as C. In this section, we do the necessary encapsulation for the code in the previous section. By the way, we can alsounsafeThe code is packaged intosafeAPI for.

I put the source code of the previous section inhere, you can check it out.

Remember the last section, we usedlibcFunctions insocketbindconnectAnd structuresockaddrsockaddr_inin_addrWait, it’s defined on rust’s side. In fact, almostlibcFunctions in,libcThis crate has been defined for us. You can goheresee. The compiler and the standard library itself also use this crite, and we also use this.

First inCargo.tomlDocument[dependencies]Add belowlibc = "0.2":

[dependencies]
libc = "0.2"

Then inmain.rsAdd above fileuse libc;Yes, you canuse libc as c;。 Or you’re simply rudeuse libc::*This is not recommended unless you know exactly where the function you are using comes from. And compare our definition withlibcDelete constants, functions and structures used in. Add againlibc::orc::To where we use constants, structures, and functions. If you are directuse libc::*, there is almost nothing to do except to delete that part of the code directly. Current code:

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);
    }
}

When you compile and run, you should get the same result as in the previous section.

Next, we try to encapsulate the functions in the above code into a more rust style API. In addition to TCP, we also need to consider adding UDP, UNIX domain and SCTP. At the same time, we’re with standard librarynetRelated APIs maintain a consistent style. We don’t consider cross platform for the time being, but only Linux, so we can boldly add some unique APIs of Linux.

Everything in UNIX is a file, and sockets are no exception. The read and write functions on the byte stream socket behave differently from the usual file I / O. The number of input or output bytes for calling read and write on the byte stream socket may be less than that requested. The reason for this phenomenon is that the buffer used for the socket in the kernel may have reached the limit. However, this is not what we really care about. Let’s look at the standard libraryFileImplementation of:

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)
    }
    ...
}

FileIs a tuple structure, and the standard library has been implementedreadandwrite, andduplicateduplicateUseful for copying out a new descriptor. Let’s keep watchingFile“Wrapped” inFileDesc:

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(())
            }
    }
}

This layer should be the end, you can see, in rustFileIt’s also directly rightlibcBut you don’t have to worry. It was mentioned at the beginning that the ABI of rust is compatible with the ABI of C, which means that it is almost zero overhead for rust and C to call each other.FileDescofreadandwriteThe implementation in is the same as beforesockfdofreadandwriteBasically the same. exceptreadandwriteBesides, there are two very useful methodsset_cloexecandset_nonblocking

I call a function “attached” to a type a method. Unlike ordinary functions, a function attached to a type must be called through the type it is attached to. Rust implements OOP in this way, but unlike OOP in some languages, this implementation of rust is zero overhead. That is, attaching some functions to a type will not cause additional overhead to the runtime, which will be handled at compile time.

set_cloexecMethod sets the descriptorFD_CLOEXEC。 We often encounter the need for fork subprocesses, and the subprocesses are likely to continue exec new programs. Set descriptorFD_CLOEXEC, which means that when we fork the child process, the same file descriptor in the parent and child processes points to the same item in the system file table. However, if we call exec to execute another program, we will replace the body of the child process with a new program. In order to avoid unnecessary trouble, we will set the open descriptor laterFD_CLOEXEC, unless special circumstances are encountered.

set_nonblockingIt is used to set the descriptor to non blocking mode if we want to use APIs such as poll and epoll.

Now that the standard library is encapsulatedFileDesc, I want to use it directly, howeverFileDescIt is invisible outside the standard library. If usedFileIf so,set_cloexecandset_nonblockingWe still have to write it again, butFileIt’s not my own type. I can’t give it directlyFileFor additional methods, you need an additional tarit or wrap it with a “myself” type. It’s very winding. In that case, let’s do it ourselves. However, we already have a reference, which can be used in the standard libraryFileDecsCopy it directly, and then remove the code irrelevant to Linux. Of course, you can play it freely.

Note that this code also calls a functioncvt, we also copied the relevant codes:

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,
        }
    }
}

Remember the one we used in the last sectionlast_os_error()Method, this code passes through the macroimpl_is_minus_onebyi32And other common typesIsMinusOnethisTaritThen we can usecvtFunctions are more convenient to calllast_os_error()Get error. I put this code inutil.rsFile, and inmain.rsAdd above filepub mod util;

Then look againFileDescFinal realization:

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) };
    }
}

I’ve been withLinuxIrrelevant code was deleted. Original reasonduplicateIt’s so lengthy because the old Linux kernel doesn’t support itF_DUPFD_CLOEXECThis setting.fcntlThis function is used to set the options to control the file descriptor. We will encounter the function to set and obtain the socket latergetsockoptandsetsockopt。 alsoread_atandwrite_atAnd other functions with complex implementation, which we can’t use, we also delete them. alsoimpl<'a> Read for &'a FileDesc , because there is an unstable API inside, I also removed it.

I gave a free play to:

pub struct FileDesc {
    fd: c_int,
}

Replaced with:

pub struct FileDesc(c::c_int);

They are equivalent. I wonder if you noticed, I putpub fn new(...)The function is removed because this function isunsafeYes — if we use these codes as libraries for others in the future, they may pass in a nonexistent descriptor, which may cause the program to crash — but they don’t necessarily know. We can do this by prefixing this function withunsafeTo tell the user that this function isunsafeOf:pub unsafe fn new(...)。 However, the developers of rust have taken this into account. We use the conventional methodfrom_raw_fdTo replacepub unsafe fn new(...), so we have the following paragraph:

impl FromRawFd for FileDesc {
    unsafe fn from_raw_fd(fd: c::c_int) -> FileDesc {
        FileDesc(fd)
    }
}

Finally, rust’sdropRealizedcloseFunction, which means that after the descriptor leaves the scope, it will automaticallyclose, we no longer need to manuallycloseYes. First off isinto_rawMethod means to putFileDescConvert to “raw” or “bare” descriptor, that is, the descriptor of C. Called in this methodforget, after the variable leaves the scope, it will not be calleddropYes. When you use this method to get the descriptor, don’t forget to use it manuallycloseOr againfrom_raw_fd

pub fn into_raw(self) -> c::c_int {
        let fd = self.0;
        mem::forget(self);
        fd
}

I put this code in a new filefd.rsIn, and inmain.rsAdd above filepub mod fd;

Then we need another oneSocketType, willsocketbindconnectWait for the function to be attached. This step should be much simpler. At the same time, you will find that we haveunsafeThe code is encapsulated intosafeCode for.

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()
    }
}

I have used the five main socket related functions in the previous section, plusreadwrite, and other functions set by descriptors are “attached” toSocketCome on. Save insocket.rs In the file.

To be clear, I’mnewandacceptMethod, byflagsSet directly for the newly created descriptorSOCK_CLOEXECOption. If you don’t want to set it in one step, you need to create a descriptor and call it againset_cloexecmethod.bindIn, callc::bindPreviously, I set an option for socketsSO_REUSEADDR, which means to allow reuse of local addresses. It will not be discussed here. If you are careful, you will find that the example in the previous section may appear if the socket is not closed normallyerror:98,Address already in use, wait a minute.accept4It’s not a standard method. Only Linux supports it. We don’t consider compatibility for the time being.setsockoptandgetsockoptMethod involves type conversion. Combined with the previous examples, it should not be difficult for you here. exceptfrom_raw_fd, I gave it againSocketAnother conventional method is realizedas_raw_fd

I’ve put it far awayhere, you can check it out. You can also try to modify the example in the previous section to the one we encapsulated todaySocket。 This section ends here.

Recommended Today

Seven Python code review tools recommended

althoughPythonLanguage is one of the most flexible development languages at present, but developers often abuse its flexibility and even violate relevant standards. So PythoncodeThe following common quality problems often occur: Some unused modules have been imported Function is missing arguments in various calls The appropriate format indentation is missing Missing appropriate spaces before and after […]