【sylar-webserver】9 网络模块
目录
Address
类图
知识点
常用结构体
常用函数
Socket
类图
主要功能
初始化 socket 流程:
ByteArray
知识点
zigzag 算法
TLV 编码结构
Stream
类图
Stream 流结构,提供字节流读写接口
SocketStream
TcpServer
类图
主要功能
Address
类图
功能:
- sylar::Address::GetInterfaceAddresses 查询所有的网卡 / 指定网卡。通过 getifaddrs
- sylar::Address::Lookup 进行网络地址解析(包括字符串形式的域名/主机名或是数字格式的IP地址,支持端口和服务名解析)。
- sylar::Address::LookupAny 通过host地址返回对应条件的任意Address
- sylar::Address::LookupAnyIPAddress,通过host地址返回对应条件的任意IPAddress
- sylar::IPAddress::Create 通过域名,IP,服务器名创建IPAddress。通过 getaddrinfo 解析地址是否是 IPv4 / IPv6
- sylar::UnixAddress 创建 UnixAddress
知识点
常用结构体
- ifaddrs
struct ifaddrs {struct ifaddrs *ifa_next; // 指向列表中下一个结构的指针。该字段在列表的最后一个结构中为 NULLchar *ifa_name; // 接口名称unsigned int ifa_flags; // 提供有关接口的一些信息的标志struct sockaddr *ifa_addr; // 接口地址struct sockaddr *ifa_netmask; // 接口的网络掩码union {struct sockaddr *ifu_broadaddr; // 接口广播地址struct sockaddr *ifu_dstaddr; // 点对点目的地址 } ifa_ifu;#define ifa_broadaddr ifa_ifu.ifu_broadaddr#define ifa_dstaddr ifa_ifu.ifu_dstaddrvoid *ifa_data; // 特定地址族数据的缓冲区
};
- sockaddr
struct sockaddr {ushort sa_family; // 网络协议char sa_data[14];
};
- sockaddr_in
struct sockaddr_in {short sin_family; // 网络协议unsigned short sin_port; // 端口struct in_addr sin_addr; // ipchar sin_zero[8];
};struct in_addr {unsigned long s_addr; // 使用 inet_aton() 加载
};
- sockaddr_in6
struct sockaddr_in6 {sa_family_t sin6_family; /* 网络协议 */in_port_t sin6_port; /* 端口 */uint32_t sin6_flowinfo; /* IPv6 流信息 */struct in6_addr sin6_addr; /* ip */uint32_t sin6_scope_id;
};struct in6_addr {unsigned char s6_addr[16]; /* IPv6 address */
};
我们发现结构 sockaddr 和 sockaddr_in 字节数完全相同,都是16个字节,所以可以直接强转。
但是结构 sockaddr_in6 有28个字节,为什么在使用的时候也是直接将地址强制转化成(sockaddr*)类型呢?
这几个结构在作为参数时基本上都是以指针的形式传入的
我们拿函数bind()为例,这个函数一共接收三个参数:
第一个为监听的文件描述符。
第二个参数是sockaddr*类型。
第三个参数是传入指针原结的内存大小。
所以有了后两个信息,无所谓原结构怎么变化,因为他们的头都是一样的,也就是uint16 sa_family,那么我们也能根据这个头做处理。
- addrinfo
struct addrinfo {int ai_flags; // 地址信息标志int ai_family; // 地址族(AF_INET, AF_INET6, AF_UNSPEC)int ai_socktype; // 套接字类型(SOCK_STREAM, SOCK_DGRAM)int ai_protocol; // 协议号(IPPROTO_TCP, IPPROTO_UDP),或0表示任意协议socklen_t ai_addrlen; // 地址长度struct sockaddr *ai_addr; // 网络地址结构指针char *ai_canonname; // 规范名字(主机名或服务名)struct addrinfo *ai_next; // 指向下一个addrinfo结构的指针
};
通过调用getaddrinfo()函数可以填充并返回一个或多个addrinfo结构,其中包含了特定主机名和服务名对应的可用地址信息。
常用函数
- getifaddrs / freeifaddrs
getifaddrs
是 C 语言中用于获取本地网络接口(如网卡、虚拟接口)信息的函数。它返回一个链表,其中每个节点包含接口的名称、IP 地址、子网掩码、广播地址等信息。与 getaddrinfo
(用于解析远程主机地址)不同,getifaddrs
专注于 本地网络接口的配置。⭐
#include <sys/types.h>
#include <ifaddrs.h>int getifaddrs(struct ifaddrs **ifap);
/**
* func:函数存储对ifaddrs结构的链表的引用;
* return:成功返回0,失败返回-1;
*/void freeifaddrs(struct ifaddrs *ifa);
/**
* func:释放对ifaddrs结构的链表的引用;
*/
【注意】: 返回的数据是动态分配的,需要释放;
- getaddrinfo / freeaddrinfo
getaddrinfo
是 C 语言中用于处理网络地址和服务的函数,能够将 主机名(如 example.com
)和 服务名(如 http
或端口号 80
)转换为套接字地址结构(如 struct sockaddr_in
或 sockaddr_in6
)。它是网络编程中替代旧函数 gethostbyname
和 getservbyname
的现代方法,支持 IPv4/IPv6 和协议无关性。⭐
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>int getaddrinfo(const char *node, // 主机名或IP地址字符串(如 "example.com")const char *service, // 服务名或端口号字符串(如 "80" 或 "http")const struct addrinfo *hints, // 输入提示(期望的地址类型)struct addrinfo **res // 返回结果的链表
);void freeaddrinfo(struct addrinfo *res); // 释放内存
- getnameinfo
getnameinfo
是 C 语言中用于将 套接字地址结构(如 struct sockaddr_in
或 sockaddr_in6
)转换为 可读的主机名 和 服务名 的函数。它是 getaddrinfo
的逆操作,通常用于将二进制网络地址(如 0x7F000001
)转换为人类可读的字符串(如 "127.0.0.1"
或 "http"
)。支持 IPv4/IPv6 和协议无关性。
#include <sys/socket.h>
#include <netdb.h>int getnameinfo(const struct sockaddr *sa, socklen_t salen,char *host, size_t hostlen,char *serv, size_t servlen, int flags);
/**
* func:将套接字地址转换为相应的主机和服务并返回;
* @param sa:套接字地址结构,保存输入的IP地址和端口号;
* @param salen:sa的长度;
* @param host:调用者分配缓冲区;
* @param hostlen:host长度;
* @param serv:调用者分配缓冲区;
* @param servlen:serv长度
* @param flags:NI_NAMEREQD:若无法确定主机名,则返回一个错误;NI_DGRAM:基于数据报(UDP)而不是基于流(TCP)的;NI_NOFQDN:只返回本地主机的完全限定域名的主机名部分;NI_NUMERICHOST:返回主机名的数字形式;NI_NUMERICSERV:返回服务地址的数字形式;* return:成功返回0,【节点和服务名称将使用以空字符结尾的字符串填充】;失败设置errno:EAI_AGAIN: 无法解析该名称, 稍后再试;EAI_BADFLAGS: flags参数的值无效;EAI_FAIL: 不可恢复的错误;EAI_FAMILY: 无法识别地址族,或指定地址族的地址长度无效EAI_MEMORY: 溢出;EAI_NONAME: 名称不能解析所提供的参数EAI_OVERFLOW: 溢出;EAI_SYSTEM: 系统错误;
*/
【注意】:调用者可以通过提供一个NULL host(或serv)参数或一个零hostlen(或servlen)参数来指定不需要主机名(或不需要服务名)。 但是,必须请求至少一个主机名或服务名。
Socket
类图
主要功能
- 封装 socketfd,以及绑定的的本地地址和远端地址
- 设置 SendTimeout,RecvTimeout
- accept,bind,connect,reconnect,listen,close,send,sendTo,recv,recvFrom
- getRemoteAddress,getLocalAddress
- 取消 socketfd 绑定的 读事件 / 写事件 / accept(读事件)/ 全部事件。直接触发
- Socket 延迟初始化,Socket()默认构造 m_sock = -1
- accept,new Socket() --> ::accept(得到 socketfd) --> init --> initSock / getLocalAddress / getRemoteAddress
- 初次 bind, newSock() ---> socket(得到 socketfd) ---> initSock / getLocalAddress / getRemoteAddress
- 初次 connect,同 bind 相同操作
初始化 socket 流程:
sylar::Socket::CreateTCPSocket(); --> Socket::ptr sock(new Socket(IPv4, TCP, 0)); ---> 空的socket,m_sock = -1sylar::Address::LookupAnyIPAddress("www.baidu.com") ---> Address::Lookup ---> getaddrinfosocket->connect(addr); ---> 延迟初始化 newSock() ---> m_sock = socket(m_family, m_type, m_protocol);| initSock(); ---> setOption(SOL_SOCKET, SO_REUSEADDR, val);| // 避免 TCP 粘包| // 如果套接字类型为流式套接字(SOCK_STREAM),则禁用Nagle算法(TCP_NODELAY)v if(m_type == SOCK_STREAM){ setOption(SOL_SOCKET, TCP_NODELAY, val); }( 两种 connect )::connect(m_sock, addr->getAddr(), addr->getAddrLen() ::connect_with_timeout(m_sock, addr->getAddr(), addr->getAddrLen(), timeout_ms) | v// 客户端 connect 连接成功后,m_sock绑定了本地地址和远端地址getRemoteAddress(); ---> getpeernamegetLocalAddress(); ---> getsocknamesocket->send(buff, sizeof(buff)); ---> ::send(m_sock, buffer, length, flags);
socket->recv(&buf[0], buf.size()); ---> ::recv(m_sock, buffer, length, flags);
ByteArray
序列化 / 反序列化 操作
字节数组容器,提供基础类型的序列化与反序列化功能。
ByteArray的底层存储是固定大小的块,以链表形式组织。每次写入数据时,将数据写入到链表最后一个块中,如果最后一个块不足以容纳数据,则分配一个新的块并添加到链表结尾,再写入数据。ByteArray会记录当前的操作位置,每次写入数据时,该操作位置按写入大小往后偏移,如果要读取数据,则必须调用setPosition重新设置当前的操作位置。
ByteArray支持基础类型的序列化与反序列化功能,并且支持将序列化的结果写入文件,以及从文件中读取内容进行反序列化。ByteArray支持以下类型的序列化与反序列化:
- 固定长度的有符号/无符号8位、16位、32位、64位整数
- 不固定长度的有符号/无符号32位、64位整数
- float、double类型
- 字符串,包含字符串长度,长度范围支持16位、32位、64位。
- 字符串,不包含长度。
以上所有的类型都支持读写。
ByteArray还支持设置序列化时的大小端顺序。
实际使用时,通常先通过 getWriteBuffers 获取到一段可写缓存,然后写入数据,手动 setPosition 设置实际的缓存偏移。
知识点
zigzag 算法
用于压缩较小的整数,参考:小而巧的数字压缩算法:zigzag_简单的老王-CSDN博客_zigzag编码
ByteArray在序列化不固定长度的有符号/无符号32位、64位整数时使用了zigzag算法。
简单来说,编码过程:负数变成 2 *(-a) - 1 ,转换为正奇数;正数变成 2 * a,转换为正偶数;解码过程,反操作即可。
TLV 编码结构
用于序列化和消息传递,指Tag(类型),Length(长度),Value(值),参考:TLV编码通信协议设计 - Tango 博客 | Tango Blog。
ByteArray在序列化字符串时使用TLV中的Length和Value。
Stream
类图
- Stream 流结构,提供字节流读写接口
所有的流结构都继承自抽象类Stream,Stream类规定了一个流必须具备read/write接口和readFixSize/writeFixSize接口,继承自Stream的类必须实现read/write接口。Stream 里的 readFixSize/writeFixSize 通过调用重写的 read/write 实现。
SocketStream
对 Socket 进一步管理,使用通用接口 read / write 对 Socket 进行操作
HttpSession 见 HTTP 模块。HttpSession 代表 服务器 accept 的部分,HttpConnection 代表客户端 connect 的部分。
TcpServer
类图
主要功能
TcpServer 对 Socket,IOManager,进行了管理
支持对多个 Address 进行监听:
TcpServer::bind(addrs, fails) ---> 遍历addrs,根据addr的Family创建一个相同Family的TCP Socket | vsock->bind(addr) ---> Socket::newSock()初始化socket| vsock->listen()
m_acceptWorker,m_ioWorker 两个 iom,分别管理 accept 事件,accpet 后 client 的处理。
TcpServer::start() ---> m_acceptWorker->schedule(std::bind(&TcpServer::startAccept, shared_from_this() , i));// m_acceptWorker 处理 所有 Socket 的 Accpet 操作,(读操作)
TcpServer::startAccept(Socket::ptr sock) ---> Socket::ptr client = sock->accept(); ---> m_ioWorker->schedule(std::bind(&TcpServer::handleClient, shared_from_this(), client));// m_ioWorker 处理 所有 accpet 后对 client 的操作,子类负责重写handleClient方法,实现不同操作。
这里的网络模块,比较简单,后续待优化⭐