From 7b17a4a33b6c77ba17467248450486472c4d1bfe Mon Sep 17 00:00:00 2001 From: ge Date: Mon, 13 Apr 2026 04:02:42 +0300 Subject: [PATCH] upd5 --- README.md | 4 -- const_linux.c.v | 22 +++++++++ examples/echo_server.c.v | 47 ------------------ examples/tcp_echo_client.v | 42 ++++++++++++++++ examples/tcp_echo_server.v | 99 ++++++++++++++++++++++++++++++++++++++ mkconst.sh | 3 +- netio.v | 3 ++ socket.c.v | 44 ++++++++++++++++- socket_addr.c.v | 62 ++++++++++++++++-------- 9 files changed, 253 insertions(+), 73 deletions(-) delete mode 100644 examples/echo_server.c.v create mode 100644 examples/tcp_echo_client.v create mode 100644 examples/tcp_echo_server.v diff --git a/README.md b/README.md index 03fda7f..82dbbcc 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,8 @@ # Networking Library for V **netio** is a flexible networking library for V programming language. -It heavily relies on the libc. Differences with the V standard library `net` module: * Provides the low-level wrappers around C API. * Supports any kind of sockets, socket options, address families and protocols. - -**netio** also provides a high-level abstractions over BSD sockets: `TCPListener`, -`TCPStream`, `UDPSocket`. diff --git a/const_linux.c.v b/const_linux.c.v index a1f694e..a8ae9a2 100644 --- a/const_linux.c.v +++ b/const_linux.c.v @@ -216,6 +216,28 @@ pub const ip_ttl = SocketOption(C.IP_TTL) pub const ip_unblock_source = SocketOption(C.IP_UNBLOCK_SOURCE) pub const ip_unicast_if = SocketOption(C.IP_UNICAST_IF) pub const ip_xfrm_policy = SocketOption(C.IP_XFRM_POLICY) +pub const msg_batch = MsgFlag(C.MSG_BATCH) +pub const msg_cmsg_cloexec = MsgFlag(C.MSG_CMSG_CLOEXEC) +pub const msg_confirm = MsgFlag(C.MSG_CONFIRM) +pub const msg_ctrunc = MsgFlag(C.MSG_CTRUNC) +pub const msg_dontroute = MsgFlag(C.MSG_DONTROUTE) +pub const msg_dontwait = MsgFlag(C.MSG_DONTWAIT) +pub const msg_eor = MsgFlag(C.MSG_EOR) +pub const msg_errqueue = MsgFlag(C.MSG_ERRQUEUE) +pub const msg_fastopen = MsgFlag(C.MSG_FASTOPEN) +pub const msg_fin = MsgFlag(C.MSG_FIN) +pub const msg_more = MsgFlag(C.MSG_MORE) +pub const msg_nosignal = MsgFlag(C.MSG_NOSIGNAL) +pub const msg_oob = MsgFlag(C.MSG_OOB) +pub const msg_peek = MsgFlag(C.MSG_PEEK) +pub const msg_proxy = MsgFlag(C.MSG_PROXY) +pub const msg_rst = MsgFlag(C.MSG_RST) +pub const msg_sock_devmem = MsgFlag(C.MSG_SOCK_DEVMEM) +pub const msg_syn = MsgFlag(C.MSG_SYN) +pub const msg_trunc = MsgFlag(C.MSG_TRUNC) +pub const msg_waitall = MsgFlag(C.MSG_WAITALL) +pub const msg_waitforone = MsgFlag(C.MSG_WAITFORONE) +pub const msg_zerocopy = MsgFlag(C.MSG_ZEROCOPY) pub const ni_dgram = NameInfoFlag(C.NI_DGRAM) pub const ni_maxhost = NameInfoFlag(C.NI_MAXHOST) pub const ni_maxserv = NameInfoFlag(C.NI_MAXSERV) diff --git a/examples/echo_server.c.v b/examples/echo_server.c.v deleted file mode 100644 index 8c2ea67..0000000 --- a/examples/echo_server.c.v +++ /dev/null @@ -1,47 +0,0 @@ -import netio - -fn main() { - ai := netio.translate_addr( - service: '1081' - sock_type: netio.sock_stream - family: netio.af_inet6 - flags: netio.ai_passive - )! - - mut socket := netio.Socket{} - mut listen_addr := netio.SocketAddr{} - - for a in ai { - socket = netio.Socket.new(a.family, a.sock_type, a.protocol)! - socket.set_option(netio.ipproto_ipv6, netio.ipv6_v6only, 0)! - socket.bind(a.addr) or { - socket.close()! - continue - } - listen_addr = a.addr - break - } - - defer { - socket.close() or { panic(err) } - } - - socket.listen(10) or { - eprintln('LISTEN: ${err}') - exit(1) - } - - println('Listening on ${listen_addr}...') - for { - conn, remote_addr := socket.accept() or { - eprintln('ACCEPT: ${err}') - exit(1) - } - eprintln(netio.translate_name(remote_addr, - flags: netio.ni_numerichost | netio.ni_numericserv - )!) - eprintln(netio.translate_name(remote_addr)!) - eprintln('Remote address: ${remote_addr}') - conn.close()! - } -} diff --git a/examples/tcp_echo_client.v b/examples/tcp_echo_client.v new file mode 100644 index 0000000..b8a7200 --- /dev/null +++ b/examples/tcp_echo_client.v @@ -0,0 +1,42 @@ +import netio + +fn main() { + // Create new TCP socket. + mut socket := netio.Socket.new(netio.af_inet, netio.sock_stream, 0)! + + defer { + socket.close() or { panic(err) } + } + + // Create the server socket address. + server_addr := netio.SocketAddr.ipv4([u8(127), 0, 0, 1]!, 1081) + + // Connect socket to the server address. + socket.connect(server_addr) or { + eprintln('CONNECT: ${err}') + exit(1) + } + + eprintln('Connected to server ${server_addr}...') + + // Send message to the server. + msg := 'Hello from client!' + + sent := socket.send(msg.bytes(), 0)! + + eprintln('Sent to the server: ${sent} bytes, data: ${msg}') + + // Read the server reply. + mut buf := []u8{len: 512} + + read := socket.recv(mut buf, 0) or { + eprintln('RECV: ${err}') + exit(1) + } + + if read > 0 { + eprintln('Received from server: ${read} bytes, data: ${buf.bytestr()}') + } else if read == 0 { + eprintln('Server closed the connection.') + } +} diff --git a/examples/tcp_echo_server.v b/examples/tcp_echo_server.v new file mode 100644 index 0000000..c3533e4 --- /dev/null +++ b/examples/tcp_echo_server.v @@ -0,0 +1,99 @@ +import netio + +fn main() { + // We want to bind a server socket to the all available local addresses, + // (both IPv4 and IPv6) so collect the address info entries for it. + ai := netio.translate_addr( + service: '1081' // The port number for listen. + sock_type: netio.sock_stream // Address must support TCP transport. + family: netio.af_inet6 // IPv6 support. + flags: netio.ai_passive // Passive mode for binding to any address (0.0.0.0, ::). + )! + + // Just initialize variables. + mut socket := netio.Socket{} + mut listen_addr := netio.SocketAddr{} + + // Create socket and bind to the first available address. + for a in ai { + // Create a socket with advertised parameters. + socket = netio.Socket.new(a.family, a.sock_type, a.protocol)! + + // Set SO_REUSEADDR enabled. It allows a server to bind to a port that + // is still in a `TIME-WAIT` state from a previous connection. + // https://en.wikipedia.org/wiki/Transmission_Control_Protocol#Protocol_operation + socket.set_option(netio.sol_socket, netio.so_reuseaddr, 1)! + + // Allow connections through IPv4, not only IPv6. + socket.set_option(netio.ipproto_ipv6, netio.ipv6_v6only, 0)! + + // Bind socket to the address. + socket.bind(a.addr) or { + // Close previously created socket on bind error and continue with + // the next socket address. + socket.close()! + continue + } + // Set listen_addr. + listen_addr = a.addr + break + } + + // If the socket.fd is -1 this means that we does not find any socket address. + if socket.fd == -1 { + eprintln('Cannot create socket...') + exit(1) + } + + // Close the server socket on exit. + defer { + socket.close() or { panic(err) } + } + // Start listening for incoming connections on socket. + socket.listen(10) or { + eprintln('LISTEN: ${err}') + exit(1) + } + + println('Listening on ${listen_addr}...') + + // Accept the connection from remote. This is a blocking call. + // conn will store the new socket connected to the remote. + conn, remote_addr := socket.accept() or { + eprintln('ACCEPT: ${err}') + exit(1) + } + + // Close connection on exit. + defer { + conn.close() or { panic(err) } + } + + // Get remote host and port in numeric format. + remote_host, remote_port := netio.translate_name(remote_addr, + flags: netio.ni_numerichost | netio.ni_numericserv + )! + + eprintln('Accpeted connection. Remote address: ${remote_host}, remote port: ${remote_port}') + + // Read 512 bytes of data from socket. + mut buf := []u8{len: 512} // Initialize the buffer to store message. + // Receive data and write it to the buffer. + read := conn.recv(mut buf, 0) or { + eprintln('RECV: ${err}') + exit(1) + } + + // Create a string from buffer without the trailing zeros. + msg := unsafe { tos_clone(buf.data) } + + eprintln('Received from client: ${read} bytes, data: ${msg}') + + // Send reply to the client. + sent := conn.send(msg.bytes(), 0) or { + eprintln('SEND: ${err}') + exit(1) + } + + eprintln('Sent to the client: ${sent} bytes, data: ${msg}') +} diff --git a/mkconst.sh b/mkconst.sh index 6cafa0e..12d1297 100755 --- a/mkconst.sh +++ b/mkconst.sh @@ -21,7 +21,7 @@ case $system in esac KIND=$1 -KIND=${KIND:-"SocketType,SocketLevel,SocketOption,AddrFamily,AddrInfoFlag,NameInfoFlag"} +KIND=${KIND:-"SocketType,SocketLevel,SocketOption,AddrFamily,AddrInfoFlag,NameInfoFlag,MsgFlag"} echo module netio echo @@ -41,5 +41,6 @@ awk -v KIND=$KIND ' $2 ~ /^AF_/ && KIND ~ "AddrFamily" {printf "pub const %s = AddrFamily(C.%s)\n", tolower($2), $2} $2 ~ /^AI_/ && KIND ~ "AddrInfoFlag" {printf "pub const %s = AddrInfoFlag(C.%s)\n", tolower($2), $2} $2 ~ /^NI_/ && KIND ~ "NameInfoFlag" {printf "pub const %s = NameInfoFlag(C.%s)\n", tolower($2), $2} + $2 ~ /^MSG_/ && KIND ~ "MsgFlag" {printf "pub const %s = MsgFlag(C.%s)\n", tolower($2), $2} KIND ~ "Any" {printf "%s\n", $0} {next}' | sort -k 2 -V diff --git a/netio.v b/netio.v index ea612f6..e061d54 100644 --- a/netio.v +++ b/netio.v @@ -24,3 +24,6 @@ pub type AddrInfoFlag = int // Flag type for `translate_name()`. // See [getnameinfo(3)](https://man7.org/linux/man-pages/man3/getnameinfo.3.html) for details. pub type NameInfoFlag = int + +// Type for recv, recvfrom, recvmsg, send, sendto, sendmsg flags. +pub type MsgFlag = int diff --git a/socket.c.v b/socket.c.v index 46d7014..82b6d66 100644 --- a/socket.c.v +++ b/socket.c.v @@ -14,8 +14,10 @@ fn C.shutdown(i32, i32) i32 fn C.close(i32) i32 fn C.setsockopt(i32, i32, i32, voidptr, i32) i32 fn C.getsockopt(i32, i32, i32, voidptr, voidptr) i32 - -struct C.sockaddr_storage {} +fn C.recv(i32, voidptr, usize, i32) i32 +fn C.recvfrom(i32, voidptr, usize, i32, voidptr, i32) i32 +fn C.send(i32, voidptr, usize, i32) i32 +fn C.sendto(i32, voidptr, usize, i32, voidptr, i32) i32 pub struct Socket { pub: @@ -111,6 +113,44 @@ pub fn (s Socket) get_option_int(level SocketLevel, option SocketOption) !int { return result } +// receive a message from a connected socket. +pub fn (s Socket) recv(mut buf []u8, flags MsgFlag) !int { + r := C.recv(s.fd, buf.data, buf.len, flags) + if r == -1 { + return os.last_error() + } + return r +} + +// receive a message from a connected socket. +pub fn (s Socket) recv_from(mut buf []u8, flags MsgFlag) !(int, SocketAddr) { + mut sock_addr_storage := &C.sockaddr_storage{} + mut sock_addr_len := sizeof(C.sockaddr_storage) + r := C.recvfrom(s.fd, buf.data, buf.len, flags, sock_addr_storage, sock_addr_len) + if r == -1 { + return os.last_error() + } + return r, unsafe { SocketAddr.from_ptr(sock_addr_storage, sock_addr_len)! } +} + +// send a message on socket. +pub fn (s Socket) send(buf []u8, flags MsgFlag) !int { + r := C.send(s.fd, buf.data, buf.len, flags) + if r == -1 { + return os.last_error() + } + return r +} + +// send a message on socket using the dst socket address as destination instead of the socket peer address. +pub fn (s Socket) send_to(buf []u8, dst SocketAddr, flags MsgFlag) !int { + r := C.sendto(s.fd, buf.data, buf.len, flags, dst.ptr(), dst.size()) + if r == -1 { + return os.last_error() + } + return r +} + // shutdown shut downs socket send and receive operations. pub fn (s Socket) shutdown(how Shutdown) ! { if C.shutdown(s.fd, i32(how)) == -1 { diff --git a/socket_addr.c.v b/socket_addr.c.v index 5240ff3..e98dc46 100644 --- a/socket_addr.c.v +++ b/socket_addr.c.v @@ -2,6 +2,8 @@ module netio import encoding.binary +struct C.sockaddr_storage {} + // max_unix_path_size value is used to pad the sockaddr_un struct. const max_unix_path_size = $if linux { 108 @@ -11,6 +13,13 @@ const max_unix_path_size = $if linux { 104 } +pub struct SocketAddr { +mut: + data &u8 = unsafe { nil } + len int + pos int +} + // SocketAddr.ipv4 creates new AF_INET socket address. // addr must be set in network (big-endian) byte order. pub fn SocketAddr.ipv4(addr [4]u8, port u16) SocketAddr { @@ -54,14 +63,14 @@ pub fn SocketAddr.unix(path string) !SocketAddr { // the address itself, you need to do that manually. The benefit is that you can create the // any kind of socket address. // -// SocketAddr is a "constructor" for +// SocketAddr is a builder for // [sockaddr(3type)](https://www.man7.org/linux/man-pages/man3/sockaddr.3type.html) objects. -// Use this function only if you understand what you do. You must manually write the data -// for the desired socket address, ensuring the correct sizes of all types, the order of -// the fields in the struct, the byte order, and the total size of the struct. The sizes -// and byte order may vary by platform, so you'll need to keep an eye on that as well. -// A mistake while creating an address will crash your application. So this function is -// marked as `unsafe`. +// Use this function only if you understand what you do. Using the `push()` method you must +// write the data for the desired socket address, ensuring the correct sizes of all types, +// the order of the fields in the struct, the byte order, and the total size of the struct. +// The sizes and byte order may vary by platform, so you'll need to keep an eye on that as +// well. A mistake while creating an address will crash your application. So this function +// is marked as `unsafe`. // // The example below creates a sockaddr_in struct describing the loopback IPv4-address // 127.0.0.1 with port number 1080. Note the comment in the example. This is a fragment @@ -118,6 +127,9 @@ pub fn SocketAddr.new(af AddrFamily, size isize) SocketAddr { // SocketAddr.from_ptr creates new socket address by copying data from specified pointer. @[unsafe] pub fn SocketAddr.from_ptr(ptr voidptr, size isize) !SocketAddr { + if isnil(ptr) { + return error('${@METHOD}: cannot accept nil ptr') + } data := unsafe { vcalloc(usize(size)) } unsafe { vmemcpy(data, ptr, size) @@ -128,25 +140,37 @@ pub fn SocketAddr.from_ptr(ptr voidptr, size isize) !SocketAddr { } } -pub struct SocketAddr { -mut: - data &u8 = unsafe { nil } - len int - pos int -} - // family returns the socket address family. +// Note: It returns 0 if socket address is nil, see also `is_empty()`. pub fn (a SocketAddr) family() AddrFamily { - mut f := AddrFamily(0) + if isnil(a.data) { + return 0 + } + mut f := 0 unsafe { vmemcpy(&f, a.data, isize(2)) } return f } +// is_empty returns true if socket address is unspecified — the data pointer is nil or +// data contains only zeros. Empty address cannot be used in `bind` and `connect` calls. +pub fn (a SocketAddr) is_empty() bool { + if isnil(a.data) { + return true + } + if a.u8_array().all(|e| e == 0) { + return true + } + return false +} + // push appends the `inp` bytes into internal data buffer. @[unsafe] pub fn (mut a SocketAddr) push(inp []u8) ! { + if isnil(a.data) { + return error('${@METHOD}: SocketAddr is nil') + } if a.pos + inp.len > a.len { - return error('push: data overflow') + return error('${@METHOD}: data overflow') } mut i := 0 for a.pos + 1 < a.len { @@ -180,9 +204,9 @@ pub fn (a SocketAddr) u8_array() []u8 { // str returns the string representation of socket address. // Supported address families are AF_INET, AF_INET6, and AF_UNIX. -// For others a string like 'SocketAddr(0x00000000)' will be returned. -// Examples: '172.16.16.132:1080', '[fdf1:72d1:0033:0000:0000:0000:0000:0247]:25535, -// '/run/app.sock'. See also `translate_name()`. +// Examples: '172.16.16.132:1080', '[fdf1:72d1:0033:0000:0000:0000:0000:0247]:25535', +// '/run/app.sock'. For others a string like 'SocketAddr(0x00000000)' will be returned. +// See also `translate_name()`. pub fn (a SocketAddr) str() string { match a.family() { af_inet {