UNP随笔
这篇文章只是记载了我对于其中不太了解的知识。
第一章
errno值:只要某个unix函数(某个系统调用或限定为某个套接字函数)有错误发生,全局变量(线程独立)errno会被置为一个指明错误类型的值。关于如何线程独立在apue线程那章的线程特定数据。
第二章
1.TIME_WAIT状态
其存在的意义是为了解决两个问题:
1. A端发送了FIN后,收到B端的ACK和FIN,最后A端还会向B端发送一个ACK,当最后这个ACK丢失,B端会重发表示自己停止发送的FIN,以期望收到A端确认。
书中提到这个状态最多会维持2MSL,猜测也许如果2MSL没有收到ACK,就会直接关闭吧。
2. TIME_WAIT的2MSL作为一个端口和IP的冷却时间。
可能出现的情况是:A端和B端有一个连接,如果关闭A端的连接,并且立即再以A端的IP和端口建立另一个连接,会导致IP和端口的绑定失败。因为此时可能还有B端发送的数据包在网络上徘徊,如果立即建立连接,又恰好收到了这些数据,就出现异常情况。而2MSL的时长,足够B端的这些数据包在网络上被丢弃。
2.端口号
知名端口0~1023
已登记端口1024~49151
临时端口49152~65535
3.主动套接字
客户端获取一个SOCKET后,调用connect连接某个IP:端口,称为主动套接字
4.被动套接字/监听套接字
某个SOCKET绑定地址后,对其调用listen函数,将套接字转化为被动套接字。
如某个SOCKET绑定{INADDR:21}后,调用listen,变成{*:21,*:*}
这里的*是通配符,表示任意地址任意端口,左边表示服务端地址,右边表示客户端地址
第三章 套接字简介
sockaddr_in和通用套接字结构sockaddr是一样的,不够前者定义了名称,使其更容易填写。
struct sockaddr_in
{
uint8 sin_len;
sa_family_t sin_family; // 协议族
in_port_t sin_port; // 端口
struct in_addr sin_addr; // IP
char sin_zero[8];
};
struct in_addr // 一个存储ip地址的结构
{
in_addr_t s_addr;
};
1.一些字节操作函数
void *memset( void *dest, int ch, size_t count );
void *memcpy( void *dest, const void *src, size_t count );
int memcmp( const void* lhs, const void* rhs, size_t count );
2.一些字节序转换函数
htons htonl
返回网络字节序的值
ntohs ntohl
返回主机字节序的值
后缀s是16位的转换,后缀l是32位的转换
int inet_aton(const char *string, struct in_addr*addr);
将字符串string转换为32位网络字节序放入第二参数存储,成功返回1,否则0
n_addr_t inet_addr(const char *cp);
此函数来把ip字符串转换为整数
char *inet_ntoa(struct in_addr in);
将IP转换位点分十进制字符串
int inet_pton(int af, const char *src, void *dst);
从字串转到网络字节序
af是协议族
af = AF_INET
src为指向字符型的地址,即ASCII的地址的首地址 (ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在dst中。
af = AF_INET6
src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在dst中
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
将网络字节序转换为字串,同上,参数cnt则是根据协议族af的不同,传递不同的值,这两个值已经被定义为宏
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
2020年7月14日11:50:55 1,2,3章
第四章 套接字编程
socket那几个函数没什么好写的
1.关于fork和exec多进程 文件描述符引用计数
pid_t fork();
fork创建与自身一模一样的副本。同时也会共享父进程打开的所有文件描述符,并对文件描述符指向的文件表项增加引用计数。
int execvp(const char * filename,char * const argv[]);
关于exec有7个函数,只写一个比较易用的吧。和fork配合使用,fork之后调用exec启动另一个程序。详见: link.第八章
2.获取socket本地地址和对端地址
int getsockname(int sockfd, struct sockaddr * localaddr, socklen_t * addrlen);
int getpeername(int sockfd, struct sockaddr * peeraddr, socklen_t * addrlen);
这两个函数,是传入一个文件描述符,然后把文件描述符对应的ip地址和端口填写到第二参数中返回,第三参数是结构的长度。
getsockname返回本地的ip和端口,getpeername返回连接对端的ip和端口。
第五章 一个cs程序示例
这一章通过一个cs示例暴露了几个可能存在的问题,以及提出的解决方法。
1.一个问题是:系统调用被信号中断
read accept这些函数可能被调用后一直处于阻塞状态,如果此时当前进程捕获一个信号,然后进入了信号对应的处理函数,处理完毕后,有的系统会恢复被中断的如accept函数,但是有的系统不会,他导致accept函数返回小于零的值,同时errno变量被设置为EINTR,这不是错误,但是却导致了accept返回了小于零的值,这种情况需要被正确处理。
以下是书中代码的解决方法(稍作修改):
for(;;)
{
clilen = sizeof(cliaddr);
if((connfd = accept(listenfd, (sockaddr*)cliaddr, &clilen)) < 0)
{
if(errno == EINTR) continue; // 虽然accept返回值是小于零的,但是不是错误,可以继续等待accept
else strerror(errno); // 书中本来是 else err_sys("accept error"),但这个函数是自定义的
} // strerror这个函数在apue第一章可以找到,它将errno的值映射成对应的字串信息并输出
}
2.另一个问题是:多个子进程同时结束,处理僵死进程
多个子进程同时结束时,信号不会排队,信号只会被处理一次,然后返回,其余的子进程依旧处于僵死状态。要处理掉这种情况。
首先要说明两个函数:
pid_t wait(int * statloc);
pid_t waitpid(pid_t pid, int * statloc, int options);
两个函数都返回终止进程的id,wait函数如果没有等待终止的进程,会阻塞知道有进程可以终止。waitpid给了更多的控制权,可以指定等待某个进程id,也可以设置不阻塞。当waitpid第一参数是是-1时,表示等待第一个终止的进程。
书中解决代码如下(稍作修改):
void sig_chld(int signo); // sig_chld是一个自定义的处理函数,用以解决多个子进程同时结束的情况
signal(SIGCHLD, sig_chld); // 书中代码将signal包装成Signal,SIGCHLD作为僵死进程发出的信号
void sig_chld(int signo) // 发生这个信号的时候,调用sig_chld来处理
{
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WNOHANG)) > 0) // WNOHANG表示不阻塞,没有僵死进程就返回
printf("child %d terminated", pid);
reuturn;
}
这两个问题都是关于信号处理的,关于信号处理,在apue第10章有详细说明,在本文中只说明关于以上两个问题的信号处理。
3.还有一个问题是,在网络上传递二进制结构时,由于大小端的原因,会导致收发的数据不一致。两边要完成对自己大小端的检查;
2020年7月15日18:59:35 4,5章
第六章 I/O复用
阻塞式I/O:等待直到有数据可读才返回
非阻塞式I/O:无论是否有数据都立即返回,没有数据时,返回一个errno错误EWOULDBLOCK(16章详解)
I/O复用:调用select poll等轮询(注意select也是会被慢系统调用打断的)
信号驱动式I/O:让内核在描述符就绪时,发送SIGIO信号通知
异步I/O:告知内核启动某个操作,让内核把包括 把复制数据到我们自己提供的缓冲区的所有操作完成后再通知我们。前面四种I/O和异步I/O的最大区别就是 前者需要我们自己复制数据,后者让内核替我们完成数据的操作.
1.select函数
int select (int maxfd, fd_set * readset, fd_set * writeset, fd_set * exceptset, const struct timeval * timeout);
返回值
负值:select本身出错
正值:某些文件可读写或出错
0:等待超时,没有可读写或错误的文件
参数maxfd
该参数存在的意义是为了提高效率,fd_set结构一般情况下是1024个描述符的长度,这里说长度而不是字节,是因为Linux和win的fd_set结构实现不一样,没记错的话Linux是位运算实现,而win是int实现。但是不管是1024还是更长,在Linux里,产生描述符时总是产生可用的最小拿的那个,所以只要传递一个最大值,就不需要再select的时候去测试该最大值之后的描述符是否有行为。
注:传递时,要最大描述符值+1
参数readset、writeset、exceeptset
fd_set结构是一个描述符集合,用一组宏来操作
void FD_ZERO( fd_set *fdset):
// 清空结构,或者说初始化
void FD_SET(int fd, fd_set * fdset):
// 将一个描述符加入某个集合
void FD_CLR(int fd, fd_set * fdset):
// 将一个描述符从某个集合删除
int FD_ISSET(int fd, fd_set * fdset)
: // 判断一个描述符是否在某个集合
关于这三个参数,分别是读写异常。在使用时,假如有文件描述符fd1,关心fd1是否有数据可读,就通过宏将fd1加入readset集合,调用select时,将readset的指针作为参数传递,select返回后,通过FD_ISSET判断该描述符是否还在其中,在就说明该描述符有数据可读,写集合和异常集合同理
以下四个条件任一满足,描述符可读:
接收缓冲区的字节数≥接收缓冲区低水位标记
当前连接的收到了对端的FIN,处于只发不收状态,此时recv或read该套接字返回0,表示正常中断
是监听套接字,且已经完成三次握手等待accept的连接数大于0
套接字上有错误,read或recv返回负一,并设置errno值
注:当套接字有错误时,既可读又可写,且都返回-1
以下四个条件任一满足,描述符可写:
发送缓冲区字节数≥发送缓冲区低水位标记
写半部关闭,即发送了FIN
使用connect连接成功或失败
套接字上有错误,返回负一并设置errno
注:当套接字有错误时,既可读又可写,且都返回-1
描述符位于异常集合的条件:
套接字有带外数据,带外数据在24章
参数timeout
timeout是一个结构,其代码:struct timeval{long tv_sev; long tv_usec;}
这个参数有三种可能:
①该参数设为空指针,永远等待直到有描述符准备好才返回
②传入一个非0值,即tv_sev或tv_usec不为0,等待该值的固定时间返回,或者在该固定时间内有描述符准备好,也返回
③传入一个0值,但不是空指针,即tv_sev和_usec为0,检查后立即返回。
2.一些杂乱的知识点
收到对端数据,read或recv返回大于0的值
收到对端FIN,read或recv返回0,此时的行为应该是将数据全部发出去(可能会有自定义的二级缓存区)
收到对端RST,返回-1
shutdown函数
int shutdown(int sockfd, int howto);
其中howto的值有SHUT_RD SHUT_WR
对一个描述符使用,关闭套接字的读流,只发不收
对一个描述符使用,关闭套接字的写流,只收不发(发送FIN)
关于pselect和poll就不写了,select足够用了
第七章 套接字选项
1.getsockopt和setsockopt
int getsockopt(int sockfd, int level, int optname, void * optval, socklen_t* optlen);
int setsockopt(int sockfd, int level, int optname,const void * optval, socklen_t optlen);
这两个函数用来设置和获取socket选项,如果要设置socket选项最好,取得socket后尽快设置,因为经过某些函数如connect后,再设置就没有用了。
2.列一些觉得有用的选项
SO_BROADCAST
这个选项是让套接字可以发送广播包,其存在的意义是为了避免程序中错误的使用广播地址,误发广播包。
SO_LINGER
struct linger{int l_onoff; int l_linger}
设置该选项之后close的行为有所改变
close后,l_onoff = 0 (默认情况)
在套接字上不能再发送或接收请求,缓冲区的内容被发送出去,如果引用计数变为0,正常终止序列FIN,接收缓冲区的数据被丢弃
close后,l_onoff = 1,l_inger = 0
在套接字上不能再发送或接收请求,缓冲区数据被丢弃,当引用计数变为0,直接发送RST到对端,直接关闭,跳过TIME_WAIT状态,甚至没有四次挥手
close,l_onoff = 1,l_inger != 0
发送缓冲区内容发出,如果l_inger 超时前还没变为CLOSED状态(收到对方ACK),就返回错误errno值,EWOULDBLOCK,接收缓冲区的数据被丢弃
这种情况有一个小小的坑:如果套接字是非阻塞的,那么close会阻塞,直到它最后清空缓存区发送出去的数据 收到来自对端的ack,才会返回,或者超时。解决办法是用shutdown关闭写那一端,留下读的流来确认。(好乱啊,也不知道这块理解的对不对,不过看起来好像没什么用)
不过我觉得这个选项最有用的地方就是跳过TIME_WAIT状态,当然跳过TIME_WATI状态也不是什么好事,原因在第二章开头。
SO_REUSEADDR和SO_REUSEPORT
SO_REUSEADDR允许绑定不同的IP到同一个端口上
SO_REUSEPORT允许多个套接字绑定完全相同IP和端口,但是使用条件是先前绑定此IP和端口套接字也设置了这个选项(完全想不到有什么用)
SO_RCVBUF和SO_SNDBUF
这两个用来设置发送缓冲区和接收缓冲区的大小、
SO_RCVTIMEO和SO_SNDTIMEO
这两个用来个套接字的接收和发送设置超时时间
SO_RCVTIMEO影响 read readv recv recvfrom recvmsg
SO_SNDTIMEO影响 write writev send sendto sendmsg
关于写函数超时后的行为,书上也没有说,大胆猜测一波:返回-1,并且errno值被设置为适当的值(未经过验证哦)
TCP_NPDELAY
设置该选项会禁止Nagle算法
IP_HDRINCL
在原始套接字上设置这个选项,IP首部在数据上构造。但是即便自己构造首部,IP协议还是会修改某些地方,如校验和部分
3.fcntl
这个主要写了关于设置非阻塞式IO,信号驱动式IO,以及用于接收信号的属主(进程ID或进程组ID),关于细节在16 24 25章,还有一段设置非阻塞的代码:
int flags;
if((flags = fcntl(fd, F_GETFL,0)) < 0)
err_sys("F_GETFL error");
flags |= O_NONBLOCK; // O_NONBLOCK是非阻塞式
if(fcntl(fd, F_SETFL, falgs) < 0) // O_ASYNC是信号式
err_sys("F_SETFL error"); // 这里先获取,然后位运算,接着在设置。单独设置的话会清除其他所有位。
// 以下是删除非阻塞标志的代码
flags &= ~O_NONBLOCK;
if(fcntl(fd, F_SETFL, falgs) < 0)
err_sys("F_SETFL error");
2020年7月17日22:56:33 6,7章
第八章 UDP编程
尤其注意多宿主机的情况,S端应该绑定某个具体IP,而不是INADDR_ANY,C端最好connect。
UDP和TCP和connect的行为是不一样的,具体为什么要这么做,下面详细说。(多宿主机即一台机器同时有多个IP。)
1.recvfrom和sendto函数
ssize_t recvfrom(int sockfd, void * buff, size_t nbytes, int flags, struct sockaddr * from, socklen_t * addrlen);
ssize_t sendto(int sockfd, const void * buff,size_t nbytes, int flags, const struct sockaddr * from, socklen_t * addrlen);
这两个函数用来实现UDP通信,S端只要socket() -> bind(),C端只需要socket(),之后就是cs两端调用这两个函数通信。
但是是有一些问题的。
2.问题1:多宿主机情况下异常
①当一个服务端有多个IP(这里假设为IP1,IP2),C端给S端的IP1发送一个数据包,而S端绑定的IP是INADDR_ANY,那么有可能的情况是:S端给C端回复消息选择的路由出口是IP2。端又有可能在应用层实现验证,即比较 C端收到的数据包IP和端口 和 C端发送的数据包IP和端口 是否相同,这种情况下,C端会在应用层丢掉这个包。
②可能会被NAT阻挡(书上没有说这个,我也没有验证过,我在了解P2P的时候看到过这个概念,写到这里突然想起来)
3.问题2:
假若某个C端给S端发送数据的,但是S端并未启动,然后协议会返回一个ICMP的端口不可达错误,
为什么不通过sendto返回负值?书中给出以下情况解释:
当一个UDP套接字连续给三个不同的服务器发送数据,两个服务器已经启动,一个未启动,那么会返回一个端口不可达错误,如果通过sendto返回负值提示发生错误,要怎么确认到底是哪个数据包没有被成功接收呢?所以协议的行为是不返回错误值,除非对套接字调用过connect,因为connect对应了一个确定的地址,就不存在无法确定哪个数据包未被收到的问题了。
4.问题1的解决办法:
即本章的开头,绑定某个具体IP,而不是INADDR_ANY
5.问题2的解决办法及注意:
首先说明UDP和TCP和connect的行为的区别:
TCP的connect通过网络进行三次握手,而UDP的connect不通过网络,是一个本地操作,他的行为是确定对端的地址。
connect之后:
①不需要再指定目标的地址,可以调用send和recv
②也不需要recvfrom取得对端地址
③异步错误(如上文的端口不可达)会通过函数返回,而不是不做声
④connect之后,内核还会为本地端(C端)选择一个出口地址,也就是说可以在本地端(C端)IP多宿情况下,确定该通信本地端(C端)是用的哪个IP。
注:如果connect时,S端服务未启动,那么connect是不会返回错误的,因为UDP的connect是一个本地操作。
6.关于多次为UDP套接字调用connect
可以为UDP套接字更换另一个目的地址。不需要重新获取描述符。
7.数据包丢失与流量控制
因为网络传输中的各种原因和UDP没有流量控制的原因,短时间大量数据包很可能会在目的地由于缓冲区限制 或 网络中间节点的能力 而造成丢包。
2020年7月19日14:31:43 8章
第九章 第十章
SCTP都被弃用了,就跳过呗。
第十一章 名字与地址转换(浑水摸鱼的一章)
关于域名系统:简单点讲就是把一个域名指向一个IP地址,多个域名也可以指向一个IP,多个IP也可用一个域名。
1.gethostbyname和gethostbyaddr(只用于IP4)
gethostbyname通过域名查询IP地址,gethostbyaddr通过IP查询域名
要注意的是,这两个函数是不可重入的,他们返回的hostent指针是一个静态变量,当下一次调用的时候数据就会被重写。
2.getservbyname和getservbyport(只用于IP4)
关于这两个函数(也是不可重入的),下午看了好久也没弄明白是什么意思。知道的是根据服务名查询端口号,也有很多问题:为什么这个函数的参数不传递IP?他到底查询的是什么?是诸如FTP之类的知名服务吗?他又向谁查询?查询范围又限定在哪里?是在局域网里吧?不然广域网岂不是要把所有机器都查一遍。
这些问题搞得一头雾水,晚上想调用一下看看到底返回些什么时,却又出现了那个找不到库的问题( link.第二条),好几个小时也没有解决。
3.getaddrinfo & freeaddrinfo & gai_strerror & getnameinfo
getaddrinfo通过 值-结果 参数返回一个链表,链表里存储协议类型IP地址端口号之类的信息,但该 值-结果 参数要在之后传递给freeaddrinfo释放。
getaddrinfo与gethostbyname相比繁琐很多,但是支持ip6。
当getaddrinfo 返回小于0的值,把该值传给gai_strerror来打印错误信息。
getnameinfo与getaddrinfo 互补,以套接字为参数,返回其中的主机字符串和服务的字符串。
其余的部分就是通过封装getaddrinfo把那些connect之类的函数实现了一遍。
2020年7月19日23:53:57 11章
第十二章 IP4和IP6的交互
IP6一般都会运行在双栈主机上,即同时支持IP4和IP6,写写他们的通信方式。
IP4客户和IP6服务器
发送IP4数据包,IP6的服务端收到后,将IP4映射为IP6,服务器并不知道是一个IP4发送过来的数据。
IP6客户和IP4服务器
IP6客户 将IP4的服务地址映射 为 IP6地址后,使用connect连接。
另外可以通过一些宏来判断地址到底是由IP4映射过来的,还有一些其他的宏来判断地址。
两个协议之间通信应该很复杂,但是协议不是我写,也没法理解人家到底如何实现的,瞎写写吧
2020年7月22日22:29:12 12章
第十三章 守护进程与inetd
1.守护进程
守护进程是在后台运行的,并且没有终端控制的进程。
因为没有控制终端,所以通过syslog函数来把日志信息交给syslogd程序记录。但是自己实现日子可控性会更高。
void daemon_init(const char * pname,int facility)
这是一个系统提供的函数,调用该函数可以把进程转换为守护进程。
首先调用fork,关掉父进程,这样进程就不是一个进程组的头进程(组长)。因为组长进程调用setsid
创建会话会失败。
然后调用setsid
创建会话。当前进程变为新会话的会话头进程,以及新进程组的组长进程。
为了确保不会有新的控制终端与其绑定,需要再次fork,因为当一个会话头进程打开一个终端设备,该终端自动称为这个会话头进程的控制终端。fork之后,新的子进程不再是会话头进程,也就不能再获取一个终端,
之后关闭描述符,改变工作目录,使用syslogd处理日志信息等等。
最终得到一个 完全独立的在后端运行的守护进程。该守护进程拥有一个独立的会话,但是却没有与任何终端相连,不会有输入进入到该进程,也不会有输出到终端,所有信息通过日志记录。第一次fork是为了避开创建会话的限制条件,第二次fork是为了之后无论如何也不会和一个终端绑定。
(关于创建会话setsid和控制终端,在link.)中第九章第三和五条有相关信息。
2.inetd守护进程
相较之下,APUE中关于守护进程概念更清晰,本书中关于细节解释更清晰。
作为网络服务程序,等待网络连接的到来,如FTP Telnet TFTP等。网络连接到来之后,他们创建一个子进程为其服务。
书中提到这样设计有一些问题:①使用出业务逻辑外几乎相同的代码②他们中大部分时间处于睡眠状态。
然后展示了一个通过IO复用,调用select的函数的模型来处理所有网络连接,这样把所有服务都集到一个进程中服务,当网络连接到来,在fork子进程为其处理。以解决上述的两个问题。
2020年7月23日11:37:12 13章
第十四章 高级IO函数
1.套接字超时的三种方式
①调用alarm,原理是在调用某个慢系统调用前,设置定时器,定时结束前慢系统调用成功返回,那么调用alarm(0),取消定时。如果定时器超时,那么慢系统调用被SIGALRM信号打断,errno设置EINTR。
②使用select轮询
③为套接字设置超时选项SO_RECTIMEO和SO_SNDTIMEO。如果recv、send等返回负值,有可能超时,通过判断errnoerrno == EWOULDBLOCK
,若成立则超时,否则可能是其他错误。
第一种使用起来不太方便,后两个的区别是:select可作用于所有描述符,而套接字选项只能作用于套接字,套接字选项不能作用于connect。
2.readv和writev的分散读和集中写
ssize_t readv( int fd, const struct iovec *iov, int cnt );
ssize_t writev( int fd, const struct iovec *iov, int cnt );
后两个参数是iovec数组和数组长度。iovec本身存储一个数据地址和数据长度:struct iovec{void * iov_base; size_t iov_len};
3.recvmsg和sendmsg
ssize_t recvmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
尤其要注意区分参数的flags和msg.msg_flags两个标志的区别(本章第五节)。
关于msghdr结构及其各个成员在代码中解释:
struct msghdr {
void *msg_name; // 前两个字段用作udp,类似于recvform和sendto的发送、接收地址
socklen_t msg_namelen; // 指针指向sockaddr_in结构,msg_namelen说明了该结构的长度
struct iovec *msg_iov; // 三四参数像readv和writev一样
int msg_iovlen; // 是接受或发送数据的数组以及数组长度。
void *msg_control; // 五六参数是关于辅助数据的。辅助数据如何处理在下一小节
socklen_t msg_controllen; // 因为可能有多个辅助数据,辅助数据用一个结构表示,该参数表示所有辅助数据的总长度
int msg_flags; // 关于该成员和recvmsg的参数flag,本章第五节详述。
};
4.辅助数据
这里主要说明msghdr.msg_control
和msghdr.msg_controllen
两个成员。
msghdr.msg_control
指向一个或多个cmsghdr结构,msghdr.msg_controllen
存储了所有结构加起来的总长度。
struct cmsghdr{
socklen_t cmsg_len; // 用来填写该结构的长度,因为最后一个成员的长度不确定
int cmsg_level; // 关于二三参数,在下面列个表格说明
int cmsg_type; // 这两个参数配合说明是什么样的辅助数据。
char cmsg_data[]; // 不知道这里是如果实现的,不指明长度,
// 让我想起了在哪看到过一个把char数组放最后,就可以越界写,但这种方法很危险。
// 如果是安全的,那么大概是先在有数据的情况下申请正好装下数据的空间,接着转换成cmsghdr操作
// 填写cmsg_len等成员,这样做的话就不会越界了,大概是这样子操作?
};
关于cmsg_level和cmsg_type参数:
协议 cmsg_level cmsg_type 说明
IP4 IPPROTO_IP IP_RECVDSTADDR 随UDP数据报接收目的地址
IP_RECVIF 随UDP数据报接收接口索引
unix域协议 SOL_SOCKET SCM_RIGHTS 发送/接收描述符
发送/接收用户凭证
IP6 关于IP6就不写了
通过一组宏来操作处理该结构:
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
取msghdr的msg_control成员,返回指向第一个cmsghdr结构的指针,否则返回NULL
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);
根据两个参数,确定结构中下一个cmsghdr,并返回,否则NULL
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);
取出结构中的数据并返回
size_t CMSG_LEN(size_t length);
看不懂这个,原话是:给定数据量下存放到cmsg_len中的值。
size_t CMSG_SPACE(size_t length);
一样看不懂,原话是:给定数据量下一个辅助数据对象的总的大小。这里的辅助数据对象应该指cmsghdr 结构。
一段示例代码:
struct msghdr msg;
struct cmsghdr * cmsgPtr;
...
for(cmsgPtr = CMSG_FIRSTHDR(&msg); cmsgPtr != NULL; cmsgPtr = CMSG_NXTHDR(&msg,cmsgPtr )
{
...
u_char * prt = CMSG_DATA(cmsgPtr);
...
}
5.recv send recvmsg sendmsg的flag标志参数,及其成员标志的区别说明
其中前四个只能用作recv
、send
、sendto
、sendmsg
等等的flags参数
后五个只能用作recvmsg
和sendmsg
第二参数的msghdr.msg_flags
成员
中间两个通用
MSG_DONTROUTE 目标主机在本地直达网络上,不用路由
MSG_DONTWAIT 临时设置某个套接字为非阻塞
MSG_PEEK 仅用于recv
和recvfrom
,读出数据后,被读出的数据并不从socket缓冲区删除
MSG_WAITALL 大概是要把用户提供的缓冲区填满才返回,但是可能因信号或者连接中断导致返回较少的数据
MSG_EOR 返回数据结束的一个逻辑记录,TCO不使用,看不懂
MSG_OOB 对于send表示将发送带外数据,对于recv表示即将读入带外数据而不是普通数据。
MSG_BCAST 如果返回结果有这个标志,说明是一个广播包
MSG_MCAST 如果返回结果有这个标志,说明是一个多播包
MSG_TRUNC 数据报被截断时,返回值有此标志,内核返回的数据长度比用户提供的缓冲区大
MSG_CTRUNC 辅助数据被截断时,返回值有此标志,内核返回的数据长度比用户提供的缓冲区大
MSG_NOTIFICATION SCTP使用,没用
6.其他
关于缓冲区中的数据量:
查看socket缓冲区的已到达数据量,有非阻塞IO、MSG_PEEK标志、ioctl操作。不过最后是自己建立一个第二缓冲区,只要socket缓冲区有数据就把他读到第二缓冲区,在第二缓冲区上有极大的可操作性。 套接字和IO流:
在APUE第五章有
使用Fdopen,获取一个与描述符相关的流对象。如下对同一描述符打开一个读流,一个写流。
FILE * FPIN = Fdopen(sockfd, "r")
FILE * FPOUT = Fdopen(sockfd, "w")
poll和kqueue接口:
这两个接口使用并不广泛,select和epoll也足够用了。
第十五章 unix域套接字
这一章我偷懒了。
unix域套接字实际上就是 某台机器上两条进程间通信。
曾经在muduo上看到过进程间通信只要TCP之类的,但是当时水平不够,没怎么认真看下去。
现在回过来百度下,看了看用TCP的优势,只觉得这个东西好多余,臃肿。
在这章看到有点用的东西就是那个发送描述符,在多进程下,监听进程使用sendmsg辅助数据向其他服务进程传递描述符(关于这个在APUE后面的章节我有看到,而且例子更明了一些。)。
偷懒不写了,以后遇到这种需求再回来填坑吧。PS:apue第十七章已经写好了,开开心心当个调包侠。
2020年7月30日16:22:54 14,15章
第十六章 非阻塞式IO
2020.9.19:在本章开头有一个误导,见 link.第四条。
1.设置非阻塞套接字的代码:
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
fcntl(sockfd, F_SETFL, falgs);
前两行用作将套接字设置非阻塞,第三行则恢复阻塞。
2.关于IO输入输出操作的函数:
输入:read、readv、recv、recvfrom、recvmsg
输出:write、writev、send、sendto、sendmsg
对于TCP,有数据指至少一个字节,对于UDP,有数据指一个完整的数据报。
输入操作 阻塞:没数据时投入睡眠,有数据时就复制到缓冲区
输入操作 非阻塞:没有数据时立即返回EWOULDBLOCK错误。
输出操作 阻塞:TCP有数据时,且缓冲区有空间时,数据写入缓冲区,缓冲区无空间时,阻塞直到有空间为止。UDP没有发送缓冲区,直接发送
输出操作 非阻塞:缓冲区若无空间,则返回EWOULDBLOCK错误。若有部分空间,返回写入到缓冲区的字节数。
3.非阻塞的connect
对于非阻塞对的connect是难以移植的
如果连接的本地服务器,那么会建立连接。connect返回0。
如果连接的是远端,会返回EINPROGRESS错误。
关于如何判断非阻塞connect是否连接成功:
书中是:非阻塞的connect连接失败时对于select,既可读又可写,连接成功时对于select只可写。但是这里有一个坑,那就是当连接成功后对端发来数据,那么既可读又可写的情况出现,无法判断。
四种解决方法:
①if(getsockopt(sockfd, SQL_SOCKET, &error, &len) < 0)
,这行代码的作用是调用getsockopt来处理套接字上的错误,其中error是值-结果参数(int类型即可),用作返回错误,len是error的长度。若sockfd建立连接,那么没有错误,getsockopt返回0,条件不成立。若返回<0,那么连接建立失败。
②调用getpeername,若该函数返回ENOTCONN错误,则连接建立失败,接下来选择性通过使用第①项来处理错误。
③长度为0的参数调用read,read失败则连接建立失败,成功则返回0。
④在调用connect一次,若失败返回EISCONN,则套接字已经连接。
4.非阻塞的accept
没有连接时返回-1,errno是EWOULDBLOCK错误。
对于非阻塞accept,书中还展示了一个坑,就是单线程模式模式中,若select对描述符可读,那么说明可以调用accept了,不过这时若因种种原因,客户端发来了一个RST,导致已建立连接从队列中取出,而accept又没有及时调用,当执行到accept时是没有可返回的描述符的。那么accept会阻塞或者返回错误(非阻塞下)。
虽然书中提出了总是设置为非阻塞 或者 忽略的解决方法,但是这种情况发生条件苛刻,而且随后也许会很快有连接到来而导致返回。但是我们在使用的时候不是总是专门建立一个等待连接的进程或者线程吗,然后把连接传递给其他线程或进程,不论是否阻塞都没有关系,毕竟我是专门等待连接的对吧。所以这个问题无伤大雅。
第十七章 ioctl操作
第十八章 路由套接字
第十九章 密钥套接字
关于这十六章,ioctl操作阻塞/非阻塞、带外数据、接口表、ARP表、路由表、流系统。
除了阻塞非阻塞外,其他几个可能永远都不会用到。而阻塞非阻塞在十六章也可通过fcntl控制。
关于十七十八章,也许在TCP/IPv1 还是v2上看过,同样可能永远用不到,等再把v1 v2看一遍再回来填这个坑。
第二十章 广播
广播好像没有什么好写了,就是调用setsockopt设置广播选项。缺点是不如多播控制的好。
路由器一般不转发地址是255.255.255.255的包,同时主机也不会对广播包进行响应,以免引起网络风暴。
对于一个广播包,IP字段是广播地址,链路层的是目的mac是ff:ff:ff:ff:ff:ff,因为在是链路层判断mac地址,如果不匹配则会被丢弃。
这章还提到了一个竞争条件,其实就是apue第十章信号的问题。
2020年8月1日20:29:44 16,20章
17,18,19章划水而过
第二十一章 多播
1.概
多播一般使用在局域网,因为没有统一的划分多播地址,在广域网上容易产生冲突。
不具备多播能力的主机或路由器会忽略这些帧。
一个给定套接字可以多次加入多播组,但必须是不同的多播地址。
套接字关闭时,自动从多播组退出。
多播组并不是一个真实存在的组,更类似与一个频道,有人在频道上广播,有人进入这个频道收听。A和B都收听了多播,但是A和B互相之间并不知道对方收听了。
一个多播会话基于一个多播地址,和一个端口。发送者只需取得套接字然后朝这个 多播地址:端口 的地址发送。接受者则 捆绑&加入 这个 多播地址:端口,即可收听在该多播地址上的数据。
2.多播通信流程
接收端:
①调用socket取得一个UDP套接字
②调用bind,为该套接字绑定一个地址,这个地址是sockaddr是类型,其中需要填写IP和端口字段。填写的地址需要一个多播地址,多播地址是一个D类地址。
注:捆绑多播地址是为了防止 目的端口 和 捆绑端口 相同的包到达该套接字。
注:捆绑端口是为了表明进程想要接收发往该端口的数据报。
③通过setsockopt或者书中提供mcast_join函数。使接收端套接字加入某个多播地址组。
注:加入多播地址组是为了让IP层告知链路层:让网卡不要过滤某些帧。(将D类多播地址 映射为 以太网地址(假设A),然后以太网接收目的mac为A的帧,而不是过滤丢弃)。同时链路层还有另一个操作,就是通知和它直连的多播路由器,告知他们接收这些分组。
④调用recvfrom等待数据的到来。
发送端:
①调用socket取得一个udp套接字
②通过setsockopt或者书中提供mcast_set_loop函数。关闭接收自循环,即自己发出的多播数据报不会被自己收到。
③调用sendto朝某个多播组地址发送数据。
书中给出一个例子的理解:
这个例子中建立了两个套接字,一个用于接收,一个用于发送。
将收发分为两个套接字的原因是,收的套接字必然会绑定一个多播地址,如果两个套接字削减为一个,那么在通过该套接字发送数据时,接收端收到的 源IP地址 和 目的IP地址 都会是 绑定的这个多播地址,从而无法辨识。
分为两个套接字,那么发送的套接字那个不必绑定地址,由系统选择一个外出接口IP填写。
当然,在IP多宿主机上,可以通过某个选项来指定外出接口IP。
3.多播地址及范围
多播地址
多播的地址是D类地址,224.0.0.0 ~ 239.255.255.255
224.0.0.1是特殊地址。当前局域网下 所有具有多播能力的主机、路由器、打印机等,都应该加入该组。
224.0.0.2是特殊地址。。当前局域网下 所有具有多播能力的路由器应该加入该组。(猜测应该是为了互相告知 接收转发 哪些帧,2小节接收端第三条那里)
地址的范围
地址的范围划分的作用是分开不同的网络:接口局部(本机)、链路局部(局域网)、网点局部、组织机构局部、全球。
IP6如何划分跳过。
IP4的划分通过TTL值来区分(这里只写接口局部和链路局部)。
TTL值为0,表示本机。
TTL值为1,表示链路局部,范围在224.0.0.0 ~ 224.0.0.255。其他的在P436。
4.多播映射及过滤
多播的帧,填写mac地址时不是主机的地址,而是一个前24位为01:00:5e的特殊值。
这个mac值的意义是减少接收非期望的多播包。
详细来说,mac地址太多,我们不能将其一一映射到网卡,然后由网卡来判断接收哪些丢弃哪些。这时只要收到一个帧,把该帧的目的mac通过一个哈希函数,映射到512个位或更多的位。这时链路层被通知说接收某个目的mac为A的帧,那么把A通过映射得到这512中的某个值,将其置为1,表示这个范围里的mac要收,不丢弃。从而改善了过滤范围。(这一层称为不完备过滤)
当不完备过滤后,数据包被提交到IP层,通过比对多播地址,合适的接收,不合适丢弃(这一层是完备过滤)
IP4多播地址 到 mac地址 的映射规则
mac前24位永远是01:00:5e,mac第25位永远是0,mac右边23位 被复制为 多播地址的右23位。
5.关于多播的选项
先写一下这些选项使用的结构
struct group_req
{
unsigned int gr_interface; // 本地接口的ip地址 的 索引
struct sockaddr_storage gr_group; // IP4或IP6多播地址
};
struct group_source_req
{
struct sockaddr_storage gr_source; // 本地接口的ip地址 的 索引
struct sockaddr_storage gsr_group; // IP4或IP6多播地址
unsigned int gsr_interface; // IP4或IP6本地地址
};
// 一个加入多播组的调用例子。
// 注:如果某个结构的本地接口地址为0或者是INADDR_ANY,那么自动选择一个
struct group_req req;
req.gr_interface = 0;
sockaddr addr; // 假设这个结构已经填写成一个由多播地址和端口组成的协议地址
memcpy(&req.gr_group, addr, sizeof(len));
setsockopt(sockfd, IPPROTO_IP, MCAST_JOIN_GROUP, &req, sizeof(req));
MCAST_JOIN_GROUP
在某个本地接口上加入一个多播组。setsockopt的参数:struct group_req。
MCAST_LEAVE_GROUP
离开一个多播组。setsockopt的参数:struct group_req。
MCAST_BLOCK_SOURCE
阻塞某个多播组不在接收。setsockopt的参数:struct group_source_req。
MCAST_UNBLOCK_SOURCE
解阻塞某个多播组。setsockopt的参数:struct group_source_req。
MCAST_JOIN_SOURCE_GROUP
加入某个特定源地址的多播组。setsockopt的参数:struct group_source_req。
MCAST_LEAVE_SOURCE_GROUP
离开某个特定源地址的多播组。setsockopt的参数:struct group_source_req。
IP_MULTICAST_IF
设置发送的多播包离开主机时 使用一个 指定的地址。setsockopt的参数:struct in_addr
IP_MULTICAST_TTL
设置生存周期,另一重含义是通过TTL来确定通信范围。setsockopt的参数:u_char
IP_MULTICAST_LOOP
开启禁止本地的自环。即自己发出的多播数据报不会被自己收到。setsockopt的参数:u_char
6.源特定多播
对源地址进行过滤以减少包量
2020年8月3日22:03:48 21章
第二十二章 高级UDP
本章主要讲述了如何给UDP增加可靠性以及何时使用UDP何时使用TCP。
1.何时使用UDP何时使用TCP
书中的总结是广播和多播必须用UDP,简单的请求和应答可以使用UDP。大量数据传输时不应该使用UDP。
2.给UDP增加可靠性
至少应该增加超时重传和确认的功能。
思路是:首先根据时间戳确定一次往返的时间,(往返时间+程序的处理时间),或许在乘2或者乘4。以这个时间为超时重传时间。重传时应该避免重传间隔过小,从而导致重传请求刚刚发出就收到对端的回应。
然后在加上一个序列号,来供两端互相确认是否丢包。
第二十三章 关于SCTP被跳过
第二十四章 带外数据
这章没怎么看明白,在这个link.里边看懂的(本章部分内容来自该链接)。
开头就讲带外数据被废弃,只是为了兼容。但是好不容易看懂了怎么能不写写呢。
1.概
所谓的带外数据不是真正的带外数据,更贴切的说法紧急数据。
带外数据的标记同一时间只能存在一个,因为只有一个紧急数据指针,当新的新的紧急数据标记到达,紧急数据指针被覆盖更新。关于如果覆盖,见下。
紧急数据只有一字节。
紧急数据指针指向数据的超尾,指向紧急数据接下来那一个字节。
2.是否设置SO_OOBINLINE选项的区别
当这个选项未被设置:
带外数据不会出现在TCP的流中,被放在一个额外的存储区中。使用recv(sockfd,&ch,1,MSG_OOB);
来取出它。
当这个选项被设置:
带外数据出现在TCP流中,可以随正常数据一起读出。(主要讨论这种情况。)
接下来带外数据基本都在SO_OOBINLINE选项被设置的情况下
3.带外数据发送
send(sockfd,"X", 1, MSG_OOB);
假设在这个调用之前,socket缓冲区已有2000字节。
当如此调用时,socket缓冲区插入1个字符X。此时有2001个字节。
当真正发送数据包时,由于某些原因,TCP可能会把这2001字节分成多个IP包发送,每个IP包头的URG紧急位都被设置,紧急数据指针指向超尾。这里想表达的是:一旦 TCP 知道了你要发送紧急数据,那么在接下来的数据发送中,TCP 会将所有的 TCP 报文段中的 URG 标志置位,哪怕该报文段中不包含紧急数据,这个行为会持续到紧急数据被发送出去为止。也就是接下来发送的TCP报文的URG都被置为1,直到紧急数据被发送出去。(加黑的这段来自本章开头的链接)
send(sockfd,"90X", 3,MSG_OOB);
当如此调用时,三个字节都被加入缓冲区,但是只有X是 紧急数据的那一个字节,90是正常数据,正常数据和紧急数据的区别体现在接收端。
4.带外数据接收
①使用信号处理
这种方式是通过设置信号处理函数处理,不过那个设置属主没怎么看明白,稍微写一下:
fcntl(sockfd,F_SETOWN, getpid());
recv(sockfd,&ch,1,MSG_OOB);
②使用select的异常集合处理
首先,可能已经收到URG位被设置的包了,然后描述符位于异常集合中,或者通过信号通知有紧急数据,但是 那个真正的紧急数据可能还没有到达,这时直接调用**recv(connfd, buff, sizeof(buff) - 1, MSG_OOB);**会返回EWOUDBLOCK错误,因为紧急数据那个字节不在接收缓冲区中。
正确的做法:正常调用recv。因为会停止在紧急数据上(书上说是read,不过我想既然read可以,那么recv理所应当也可以)。举例:接收缓冲区中有200个自己,第100个字节是紧急数据,那么recv返回的n是99。接下来一个字节可能就是紧急数据了。可以通过int sockatmark(int sockfd)
函数来判断,下一个字节是紧急数据,那么返回1,不是则返回0,出错返回-1。
5.标记的覆盖
在发送端,当有又一个紧急数据字节被加入流,那么原来的那个紧急数据就是普通数据了,新加入的这个这个紧急数据字节成为新的紧急数据。可想而知,发送端并不在乎紧急数据是否被覆盖更新了,我接收端只在意何时读到这个紧急数据,然后改变行为即可。
2020年8月6日20:36:45 22,24章
第二十五章 信号驱动式IO
先写结论:信号IO在TCP上几乎无法使用,很少很少很少的情况下在UDP上使用。
信号驱动式IO产生SIGIO信号。
UDP的以下事件产生SIGIO信号:
数据报到达套接字 | 套接字发生异步错误
TCP的以下事件产生SIGIO信号:
监听套接字有连接请求完成 | 断开请求发起 | 断开请求完成 | 连接办关闭 | 数据到达套接字 | 数据从套接字送走 | 发生异步错误
可见TCP如此多的行为都SIGIO信号,我们却没有办法区分,以至于无法使用。
2020年8月7日20:37:33 25章
第二十六章 线程
关于这章在apue第十一章中的描述更清晰。
第二十七章 IP选项
这章只讲了一个源路由选路的选项。书中给出例子,在发送端使用setsockopt来设置路径,在接收端调用getsockopt来取得该路径。
2020年8月8日16:57:23 27章
第二十八章 原始套接字
本章开头讲了原始套接字一些概念,之后用原始套接字实现了ping tracerout 还有一个ICMP消息守护,代码是很好的例子。
通过原始套接字,进程可以读写ICMPv4、IGMPv4、ICMPv6。
通过原始套接字,设置IP_HDRINCL套接字选项自己构造IP4首部。
1.原始套接字的创建步骤:
int sockfd = socket(AF_INET, SOCK_RAW, protocol);
const int on = 1;
// 开启IP_HDRINCL
setsockopt(sockfd, IPPROTO_IP, P_HDRINCL, &on, sizeof(on));
原始套接字可以bind和connect,但是没必要。
2.原始套接字的输出:
通过sendto或sendmsg。
若开启IP_HDRINCL,IP首部由进程构造,sendto的数据长度也要包括IP首部。
不开启IP_HDRINCL,由内核构造,IP首部的协议字段 填为 调用socket函数创建套接字时的第三参数。
IP4的首部校验和 之后的 任何校验和,都应该由用户填写。(如IP包里是一个ICMP包,ICMP的校验和应该由用户填写。)
3.原始套接字的输入:
无论何时,一个IP4原始套接字收到内核传来的数据报传递到该进程的都是包括IP首部在内的完整数据报。其中ip_len、ip_off、ip_id为主机字节序,其余为网络字节序,在linux上则全是网络字节序。
UDP和TCP分组不会被传递到原始套接字。如果要接收,需要在链路层读取。
ICMP/IGMP分组则会交给原始套接字。
无法识别的协议字段的所有IP报都传递到套接字。
如果调用socket时,第三参数为0,也没有对描述符bind或connect,那么任何不能识别协议字段的数据报都被递送。
如果bind了地址,那么接收数据报的目的地址必须匹配才会递送。
如果connect了地址,那么接收数据报的源地址必须匹配才会递送。
2020年8月9日20:54:10 28章
第二十九章 数据链路访问
数据链路的访问有三个接口以及书中提到的两个函数库。
1.三个接口:
①BSD分组过滤器
②DLPI数据链路提供者接口
③SOCK_PACKET和PF_PACKET
fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
由数据链路接收的任何协议的以太网帧都会返回到这些套接字。
fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP));
和上面那行的区别是第三参数不同,前者捕获所有协议的帧,后者捕获IP协议的帧
2.两个函数库
libpcap
该函数库与实现无关,提供了分组捕获机制的函数。从www.tcpdump.org取得。
libnet
提供构造任意协议的分组并将其输出到网络中的接口。从www.packetfactory.org取得。
关于之后的例子没有认真去看,因为这些例子调用函数库来实现了。我想的是自己写一下来更深的了解一下。好了又开坑了。
2020年8月10日21:48:29 29章
关于第三十章,粗粗的翻了一下,是关于CS设计范式的,其实主要就是讲是主线程accept,其他线程处理逻辑,之前也大概了解过这些。
关于三十一章,没怎么看明白,传输层收发消息之类的?不过看起来也不重要了。unp到此也算结束了。
接下来大概是把apue和unp这两篇笔记里的坑填一填。
2020年8月10日21:54:07