unix网络编程:大端小端、常用的函数细节、inetd介绍、select函数到epoll函数的变化

大端和小端

这个概念一般会在体系结构中碰到,我们所常用的x86架构的系统都是采取小端存储方式,而68K架构的系统都是采取大端存储方式。

小端的存储方式看起来不是很好看,先存低位数据到低地址,因此看过去是反的。大端存储方式看起来容易理解,就是正常顺序,先存高位数据到低地址。

不同机器采取的存储方式不同,在网络通信中数据也难以判断是哪种存储方式的。因此也出了一个统一的规定,在网络上数据必须按照大端的字节序传输。

我们在进行网络编程时都会用到字节序有关的库函数 <netinet/in.h>

uint16_t htons(uint16_t hs); //可以字面上理解函数的功能,host to network short,因此这个函数的功能为将主机的小端字节序存储的数据转化为网络的大端字节序存储形式的数据,并且这个数据为16位

uint32_t htonl(uint32_t hl);//小端转大端,32位

uint16_t ntohs(uint16_t ns);//大端转小端,16位

uint32_t ntohl(uint32_t nl);//大端转小端,32位

我们大多都是使用x86系统,我们在网络上传输数据的时候,首先都是需要把数据转化为大端的形式再发送,而接受来的数据需要将其转化为小端的形式再进行阅读。

socket函数

socket函数我们用于创建套接字,该函数的原型为

int socket(int af, int type, int protocol);

af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。你也可以使用 PF 前缀,PF 是“Protocol Family”的简写,它和 AF 是一样的。例如,PF_INET 等价于 AF_INET,PF_INET6 等价于 AF_INET6。

type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。

protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

有了地址类型和数据传输方式,还不足以决定采用哪种协议吗?为什么还需要第三个参数呢?

这是因为此处我们只是讲了TCP和UDP传输协议,在传输层还有其他协议,会存在有两种不同的协议支持同一种地址类型和数据传输类型。

bind函数

函数原型如下sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。

int bind(int fd, struct sockaddr *addr, socklen_t len);

在结构体sockaddr_in中,

  1. sin_family 和 socket() 的第一个参数的含义相同,取值也要保持一致。
  2. sin_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号。端口号需要用 htons() 函数转换。
  3. sin_addr 是 struct in_addr 结构体类型的变量.
  4. sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。

结构体in_addr比较简单里面存放的就是32位的ip。

struct sockaddr_in{
    sa_family_t     sin_family;   //地址族(Address Family),也就是地址类型
    uint16_t        sin_port;     //16位的端口号
    struct in_addr  sin_addr;     //32位IP地址
    char            sin_zero[8];  //不使用,一般用0填充
};
struct in_addr{
    in_addr_t  s_addr;  //32位的IP地址
};

注意:bind函数中的第二参数为sockaddr而不是sockaddr_in,sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。



可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址,它的定义如下: 

struct sockaddr_in6 { 
    sa_family_t sin6_family;  //(2)地址类型,取值为AF_INET6
    in_port_t sin6_port;  //(2)16位端口号
    uint32_t sin6_flowinfo;  //(4)IPv6流信息
    struct in6_addr sin6_addr;  //(4)具体的IPv6地址
    uint32_t sin6_scope_id;  //(4)接口范围ID
};

经典用法

struct sockaddr_in saddr;

/* Zero out the memory */

bzero(&saddr, sizeof(saddr));

saddr.sin_family = PF_INET;

saddr.sin_port = htons(1234);

saddr.sin_addr.s_addr = htonl(INADDR_ANY);

connect函数

用于客户端和服务端连接,函数原型如下,参数同bind函数

int connect(int fd, struct sockaddr *sa, socklen_t len);

我们需要注意的一点就是,以往我们这个connect函数都是用在tcp连接中的,像下面介绍的经典UDP的C/S模型中就没有connect,但是现在UDP连接也可以使用connect,并且不再使用sendto和recvfrom函数了,改用write/send函数和recv/recvmsg函数。这种形式的UDP连接在相同的一对主机频繁连接的情况下可以有效提高性能。

经典的UDP的C/S模型

 

UDP的两个主要recvfrom / sendto函数

#include <sys/socket.h>
ssize_t recvfrom(int fd, void
  *buf, size_t nbytes, int flags,
  struct sockaddr *from,
  socklen_t *len);
ssize_t sendto(int fd, void *buf,
  size_t nbytes, int flags,
  struct sockaddr *to, socklen_t len);

recvfrom函数的参数介绍:

fd:socket描述符;
buf:UDP数据报缓存区(包含待发送数据);
nbytes:UDP数据报的长度;
flags:调用方式标志位(一般设置为0);
len:from所指结构体的长度;
from:指向源数据的主机地址信息的结构体(sockaddr_in需类型转换);
len:from所指结构体的长度;

sendto函数的参数介绍:

to:指向目的主机的地址信息的结构体

UDP连接中古怪的地方

  • 可以发送0字节的包,仅仅包含8字节的head
  • recvfrom函数因此也接受0字节的数据报,注意在TCP连接中,recvfrom函数接受到0字节表明对方已经关闭连接。

listen函数

如果要建立TCP连接,通过 listen() 函数可以让套接字进入被动监听状态。函数原型如下:

int listen(int fd, int p_log);

fd表示套接字的文件描述符。p_log表示请求队列中的最大长度。

请求队列:当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。

accept函数

当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。它的原型为:

int accept(int fd, struct sockaddr *addr, socklen_t *addr_len);

函数的参数同connect函数

close函数

int close(int fildes);

十分简单

write/read数据

write可以向套接字中写入数据,使用read可以从套接字中读取数据。

write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。

read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

这两个函数的原型如下

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

fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。

size_t 是通过 typedef 声明的 unsigned int 类型;ssize_t 在 "size_t" 前面加了一个"s",代表 signed,即 ssize_t 是通过 typedef 声明的 signed int 类型。

其他一些有用的函数

inet_aton() and inet_pton()        “127.0.0.1” => sockaddr format

int inet_aton(const char *string, struct in_addr *addr);

int inet_pton(int domain, const char *restrict str, void *restrict addr);

inet_pton()函数用于将文本字符串格式转换成网络字节序二进制地址; 若成功,返回1;若格式无效,返回0;若出错,返回-1;

inet_aton()函数用于将点分十进制IP地址转换成网络字节序IP地址;

gethostbyname()     “www.google.com” => hostent
gethostbyaddr()       “172.217.10.4” => “www.google.com”

getaddrinfo() and getnameinfo()   IPv6-friendly versions

TCP连接建立分配给子进程中注意点

fork函数创建出来的是一个镜像子进程,在accept建立连接后,建立连接的socket文件描述符的引用数将会+1,这意思是父子进程使用的是同一份socket文件,父进程应当及时关闭socket,避免子进程在完成服务后,关闭了sokcet然后父进程却没有关闭而导致socket关不掉。

inetd网络daemon进程

inetd是监视一些网络请求的守护进程,其根据网络请求来调用相应的服务进程来处理连接请求。它可以为多种服务管理连接,当 inetd 接到连接时,它能够确定连接所需的程序,启动相应的进程,并把socket交给它。使用 inetd 来运行那些负载不重的服务有助于降低系统负载,因为它不需要为每个服务都启动独立的服务程序。

一般说来,inetd 主要用于启动其它服务程序,但它也有能力直接处理某些简单的服务,例如chargen、auth,以及daytime。

inetd.conf是inetd的配置文件。inetd.conf文件告诉inetd监听哪些端口,为每个端口启动哪个服务。在任何的网络环境中使用Linux系统,第一件要做的事就是了解一下服务器到底要提供哪些服务。不需要的那些服务应该被禁止掉,最好卸载掉,这样黑客就少了一些攻击系统的机会。查看“/etc/inetd.conf”文件,了解一下inetd提供哪些服务。用加上注释的方法(在一行的开头加上#号),禁止任何不需要的服务,再给inetd进程发一个SIGHUP信号。

对于TCP服务器,inetd监听在应用程序已知的端口上,监听链接请求,接受连接,映射链接到标准输入,标准输出和标准错误输出,启动适当的服务器。

对于UDP服务器,当UDP服务器的已知端口上数据可读时,inetd要求操作系统通知他,知道inetd启动的服务器中止,inetd再在已知端口上进行下一步操作。

inetd的工作流程图

 select、poll、epoll函数的介绍

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

在Linux中,我么可以通过select函数实现IO端口的复用,能够监视我们需要监视的文件描述符的变化情况,函数原型如上所示。其中的参数为

  1. 监视的文件句柄数,一般设为要监视的文件中的最大文件号加一。
  2. 监视的可读文件句柄集合,当readset集合中的文件句柄状态变成可读时系统告诉select函数返回。这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值,可以传入NULL值,表示不关心任何文件的读变化;
  3. 监视的可写文件句柄集合,当writeset映象的文件句柄状态变成可写时系统告诉select函数返回。如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,
    如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值,可以传入NULL值,表示不关心任何文件的写变化。
  4. 监视的异常文件句柄集合,当exceptset映象的文件句柄上有特殊情况发生时系统会告诉select函数返回。
  5. select()的超时结束时间。这个参数它使select处于三种状态,
    第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,
    一定等到监视文件描述符集合中某个文件描述符发生变化为止;
    第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,
    都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
    第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,
    超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

返回值为正值表明有监控的文件描述符符合条件;为负值表明出错了;为0表明超时,没有符合条件的文件描述符。

参数中的fd_set类型的变量可以通过以下函数来设置,函数作用如同字面意义。

  • void FD_SET(int fd, fd_set *s)
  • void FD_CLR(int fd, fd_set *s)

  • int FD_ISSET(int fd, fd_set *s)

  • void FD_ZERO(fd_set *s)

select函数的运行机制和问题

当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一socket或文件可读。使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。

在每次调用select函数的时候都需要进入内核态,需要将函数参数中的fd_set集合拷贝到内核空间,并对这个集合进行遍历,如果这个集合十分大的话,那么这个开销将会十分大,并且select函数在设计时也是为了避免大量数据的拷贝导致性能的损失,还设定了fd_set集合大小做了限制为1024。

poll函数

在select函数出现的那个年代,一台服务器基本不可能能过处理1000多个连接,因此这个大小限制基本不是问题,但是随着机器性能的快速提升,这个fd_set集合大小的限制开始有影响,就出现了poll函数,poll和select的机制相似,仅仅只是解决了集合大小限制的问题,并没有解决最关键的性能损失问题。

epoll函数

epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll函数的底层的操作方式为回调不再是遍历,每当有fd就绪时,系统注册的回调函数就会被调用。而poll和select都是采取的遍历。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值