linux网络编程之扫盲
1. UDP传输协议 :
1) 数据报协议:含有边界,逐级封装及其逐级解封装。
2) 不可靠:不保证数据报能能够准确的到达宿端。数据的确认,超时重传及其包的组合由应用软件来实现。
3) 无连接:客户端与服务器端不必建立唯一的套接口进行通信。如客户端可以用一个套接口给一个服务器发送数据,也可以用同一个套接口给另一个服务器发送数据;同样一个服务器可以用同一个接口接受来自不同客户端的数据。
4) 无流量控制:不管接收端是否有剩余BUFF,它都被投递,哪怕溢出丢失。
2. TCP传输协议
1) 字节流协议:不含边界,分序传送数据,到达对端,由TCP协议将其组合并递送到应用软件,。
2) 可靠:数据的确认超时(RTT)重传及其组合数据流均是由TCP协议来完成的并提交。
3) 面向连接: 维护稳定的套接口对,即使用固定的IP和端口进行一项任务传输。
4) 流量控制:TCP协议的窗口有效控制并告知对端数据的可接纳程度
5) TCP为全双工的: 应用软件即可发也可收数据,要求TCP协议能够跟踪方向数据流信息。
3. 弄懂TCP 建立连接的三路握手协议和四路终止协议及其TCP状态转换图。
TCP的TIME_WAIT2MSL时间消亡概念,主要来源两种情况:一是服务器发来FIN后客户端的ACK丢失,要做好处理服务器端的再次FIN,如果立即消亡,那么服务器端因得不到FINACK而hold住出错重传等等。二是套接口上容易复用已经使用的IP和端口号,即生成前面已使用的一个化身,那么之前的老数据包在路由上可能会被化身使用,这通常是不允许的,要避免这种情况,就要让TCP在TIME_WAIT 2MSL之后允许生成化身,那么原来老的分组在路由上就已经消亡了。
4. 套接口编程的基本函数:
1) socket(int family, int type, int protocol). AF_***, SOCK_***, IPPROTO_***,可以理解为类,类成员,类成员。成功返回非负描述符,失败返回-1。
2) connect(int fd, struct sockaddr* servaddr, socklen_t addrlen),注意是服务器的地址,成功返回0,失败返回-1
3) bind(int fd, struct sockaddr* localaddr, socclen_t addrlen),注意是本地地址,成功返回0,失败返回-1
4) listen(int fd, int listenqd),listenqd 等待队列长度。listen里面包括这种概念:已完成连接和未完成连接,listenqd就是这两种队列之和,若没有进入到完成连接队列,则将其置入内核维护的等待队列直到可以被成功连接。黑客曾经使用未完成连接来海量发送SYN淹没合法的SYN进行攻击。
5) accept(int fd, struct sockaddr* cliaddr, socklen_t* addrlen);注意是返回客户端地址,长度为值-结果参数
6) close(int fd) 只有等到fd的引用计数减为0的时候才会进行四路TCP终止序列从而结束通信,注意有一种结束函数shutdown()不需要考虑这个特点,它可以让TCP在需要时发送FIN结束分节。
7) getsockname(int fd, struct sockaddr* localaddr, socklen_t* addrlen),获取本地的套接口信息,给localaddr保存,addrlen是值-结果参数。
8) getpeername(int fd, struct sockaddr* peeraddr, socklen_t* addrlen), 获取对端套接口信息,给peeraddr地址结构保存,addrlen是值-结果参数。
5. accept()函数是一种慢系统调用函数,它是一种容易被中断的系统调用,典型的例子是,服务器程序fork后子进程,子进程退出时会向父进程发送一个SIGCHLD信号,如果该信号被父进程设置为要处理,那么当前父进程所处的accept状态即将被打断,并且返回EINTR错误,所以编写程序的时候,若要针对子进程的SIGCHLD处理,那么就要考虑这种慢系统调用状态,并且测试errno为EINTR后自己启动accept()。
强调的是connect()函数被中断后重启时不允许的,如果调用会立即返回EINTR错误,解决这个问题的方法就是用select函数等待连接的完成。
6. TCP的异常情况主要考虑以下几种可能:
1)socket,bind,listen后,在accept()函数前客户端发送RST夭折。这种情况处理依赖于不同的标准,POSIX会向服务器发送ECONNABORTED.
2)服务器进程终止,向客户端发送FIN,注意这里客户端的readline不会被阻塞,因为有FIN发送出去,但没有传送数据,客户端readline返回零并报错。服务器发送FIN后,客户端TCP发送ACK响应,此时,这个状态告诉客户端,服务器端已经关闭向客户端发送的缓存并不会再发送数据了,但仍可以接受来自客户端的数据,因为此时处于TCP半关闭状态。等到客户端发送FIN并收到服务器端的ACK后,连接终止。注意上述是在进程被杀掉的瞬间并且保证客户端有上述操作,真实的情况要复杂些,比如下面3)item.
3)服务器进程终止,但客户端多次发送数据给服务器.比如在第一次发送数据后,其实由于服务器进程已经终止,写给服务器的数据由于没有服务器进程接受,服务器端发送一个RST分节给客户端,那么第二次写的时候由于已经产生一个RST,这样不得不报错发送SIGPIPE信号和返回EPIPE错误,对这种信号一般是忽略SIG_IGN。
4)服务器崩溃,由于服务器崩溃,FIN都发不了了,readline必将阻塞死在那,TCP会在比较长的时间超时而推出。如果想快速推出,可以在readline函数实现一个较短的定时设置。
5)服务器崩溃了并重启系统。这是服务器响应客户端分节时会发送RST分节并返回ECONNRESET错误。针对4)5)两项的情况,TCP协议可以使用SO_KEEPALIVE和心搏函数来监测服务器是否已经崩溃或重启。
6)服务器关机,向服务器进程发送SIGTERM信号,服务器可以在此处理客户端的连接情况。
7. I/O操作的主要分类:
1) 阻塞I/O, 常见的read/write调用
2) 非阻塞I/O, 常见的设置为O_NONBLOCK的read/write调用
3) 复用I/O(如select和poll), 检查fd文件描述符是否就绪,若fd集里面有一个就绪,就执行I/O操作,若无就阻塞在select上直到有fd就绪为止,某种程度上讲也是属于阻塞的I/O处理,无非是能对多个fd进行监控。
4) 信号驱动I/O, 当文件描述符fd就绪的时候,发送信号SIGIO通知内核做I/O操作,这样在读写数据之前无需处于阻塞状态等待数据到达,设置好信号句柄函数就可以。
5) 异步通知I/O. 当I/O操作完成的时候,内核报告读写完成,通知对应的应用程序当前的I/O读写状态,一般也是信号通知,同信号驱动不同的是,它是在完成I/O之后异步通知的。
8. UDP 编程特点罗列:
1) 使用的系统调用: sendto()和recvfrom(), 客户服务器均使用该系统调用。 服务器可以处在以下状态:关闭,重启,终止进程,进程未打开,这些状态都会导致ACK无法回馈。此时sendto,recvfrom容易进入死等,解决方法是设置定时器。
2) 未建立连接的UDP客户程序,若服务器生成多个服务进程来处理UDP数据报请求,如果当中一个出现故障,那么无法正确返回具体的信息给客户进程用来告诉到底是哪个因素导致故障,比如无法区分是服务器宕机还是主机不可达,ICMP异步报错无法到达客户进程。要解决这个问题,需要用到已建立连接的UDP编程概念。
3) 已建立连接的UDP。它使用一对端口,注意是一对端口进行UDP客户服务器的连接,使用connect()函数附上write/send写和read/recvmsg/recv读,代替未建立连接的sendto和recvfrom。UDP是无法直接获得UDP数据报宿地址的,若要获取,必须使用recvmsg()函数获得UDP数据报宿地址。
4) UDP客户程序若没有BIND指定的IP地址和端口,那么它选择IP的时候倾向于搜索路由表分配,即接近于路由表需要的外出接口作为主IP。
9. DNS在TCP/UDP编程中的应用。
1) RR源记录分类:A(主机名转化为IPV4 32bitIP地址),AAAA(主机名转化为IPV6 128bitIP地址),PTR(ip地址转化为主机名),MX(做邮件传递服务中转名,很少用),CNAME(Canonial name取得规范化名字)
2)IPV4常用获得主机名IP及其服务名(注意不是服务器的名字)的函数有以下几个:gethostbyname,gethostbyaddr,getservbyname,getservbyaddr,这些函数是编写通信封装程序常用的调用,因为它是遍历主机接口的,这些函数主要的两个结构体是struct hostent 和structservent.
3) getaddrinfo函数可以关闭编写IPV4和IPV6关于获得主机信息的差异,它一定是未来统一的标准,要多使用该函数编程,关键的数据结构为struct addrinfo*.
4) 教科书上UNIX网络编程第二版编写了许多封装getaddrinfo函数的应用,如Tcp_connect(hostname,service), Tcp_listen(),Udp_client()面向无连接,Udp_connect()面向连接,及其Udp_server()服务器端的程序。
5) 需要注意的是上面get****函数都是不可重入的,它的函数内部包含着许多临界资源和竞争资源如静态变量和分配的内存。信号处理函数不可以调用关于这些get_***函数。
10. 守护进程的几点强化:
1)守护进程没有控制终端,它的产生通常由脚本调用执行,并且该脚本需要有特权权限。以inetd为例,系统启动获得特权权限,执行/etc/init.d/下面的脚本运行inetd守护进程,该守护进程监测FTP,HTTP流量信息等等,并将报告信息发送到日志文件当中。
2) 守护进程不会向终端传送数据,要获取守护进程的信息,通常是在日志文件中获取。处理日志的方法无非以下四种:
(1)/var/run/log获取
(2)有控制台设备/dev/console
(3)创建UDP接口绑定514端口,它是专做syslog服务端口的
(4)/dev/klog记录内核信息,打开它并读它即可
3)守护进程的几部曲:
(1) fork 子进程,退出父进程
(2) 当前子进程,即第一个子进程setsid作为会话首领。
(3) fork新子进程,称为第二个子进程,退出第一个子进程,目的是防止即使第一个子进程获得了控制终端,在新第二个进程中也不会使用。退出第一个子进程的时候,会向他衍生的子进程组发送SIGHUP信号,因此要将该信号SIG_IGN忽略掉。
(4) 在第二个子进程中,切换到chrdir("/")根工作目录,关闭由第一个子进程继承而来的文件描述符fd,关闭它有不同的方法,通常根据系统而来,而且也比较复杂麻烦,但有些系统是有专门的函数来处理者访问的,如closefrom().
4)syslogd守护进程接受来自守护进程的出错信息或者打印信息,因为守护进程没有终端的哟。编程方法:void daemon(){***;syslog();***;}调用syslog()函数,从而实现信息的保存。
5)守护进程一般不要使用诸如printf和fprintf的打印调用。
11. 高级IO函数编程:
1) socket编程常常要处理延迟问题来放弃对服务器的连接,有以下三种方法来实现:
(1) alarm报警,通过报警信号处理来完成连接断开,但使用信号的方法,极其容易引起竞争的条件。
(2) select设置timvspec定时值,作为延迟。在指定的时间内阻塞在select上,而非阻塞在IO上。
(3) 设置套接口属性SO_RECVTIMEO,SO_SENDTIMEO,但有部分系统不支持这种设置,移植性一般。
2)新的IO函数,比如recv,send函数,他们比read/write添加了一个标志位,专门面向网络套接口编程的读写,该标志位flags可以设置为MSG_NOWAIT非阻塞,MSG_PEER获取在套接口上排队的数据量,MSG_WAITALL等待数据到达才能返回。
3) 敛散读聚集写
(1) 敛散读readv(fd,struct iovec *io_vec, flags): 把单个缓冲区的数据分散在多个应用缓冲区中执行多次读操作。
(2) 聚集写writev(fd,struct iovec *io_vec,flags): 把分散在多个应用进程缓冲区的数据聚集到单个缓冲区执行一次写操作。
4) sendto,recvfrom是用在UDP套接口编程的,UDP是不可以直接获得套接口宿IP地址的,它需要recvmsg(fd,struct msghdr,int flags)函数.
5) recvmsg,sendmsg是比read/write, recv/send,readv/writev更加底层的通用的系统调用,它维护:套接口地址, 辅助数据,聚集敛散读写,引用标志操作。其中辅助数据是让UDP获取宿地址IP能力的原因,标志的操作是用引用编程完成的,可以直接反应内核改变的标志并提交给应用层,即实现所谓的值-结果传递。
6)IO操作,分为标准IO和通用IO。标准IO是指一个方便移植在非UNIX系统的库,如fopen.fclose.fread,fwrite,它是面向流的,注意是面向流的,终端和控制台也是面向流的。通用的IO是指read/write等围绕fd文件描述符进行处理的,它是内核的系统调用。标准的IO用在socket编程的时候会在套接口缓冲上增加一层缓冲,使得编程在某些时候出现不定性,一般要将其用setvbuf设置为无缓冲或者抛弃不用,改用通用的IO系统调用来处理。
7) T/TCP是TCP协议的简单增强版,它是把SYN,FIN,ACK,DATA合在一起作为一笔事物transaction来处理的。
12. 非阻塞的套接口编程:
1) 默认套接口是阻塞的,打开套接口的非阻塞性需要用fcntl控制。connect, accept均是默认阻塞的.
2) TCP connect的时候,会产生三路握手,发送SYN等待SYN和ACK,若套接口配置为非阻塞,它的三路握手仍然执行,它可以理解为这个过程:先是启动三路握手,RTT时间等待连接建立,在这段时间内可以做需要做的事情,如用select监测sockfd的状态(可读可写否?),非阻塞的connect同select是一块编程的一体的,通过getsockopt可以反馈当前非阻塞connect是否建立起来,还是有err出现,比如ECONNREFUSED,EINTR等。
3) accept()函数由于套接口编程非阻塞会表现出一些不同的特点,它有助于解决accept函数之前客户进程夭折并发送RST给服务器的情况。
4) 其实在服务器端的编程,我们不必为了实现非阻塞而对多个描述符进行select,代码比较简洁的方法可以提交给fork后的进程或者线程编程,当然这里要求有信号的帮助及其可重入特性,而且也要考虑到异常情况,如重启宕机进程死亡及其提前夭折等的处理。
13. ioctl函数可以针对网络做一系列请求,这些请求分为:套接口操作,文件操作,接口操作,ARP高速缓存操作,路由表操作及其流系统。
套接口的编程存在数据链路层的套接口编程,它的协议簇为AF_LINK,用于设置和获取链路层信息,比如PPP接口的数目。
路由套接口是让用户进程通过内核发送或接受消息,针对路由套接口发送或接受路由表信息,甚至倾泻整个路由信息。若使用ioctl(),request的选项参数为SIOADDRT, SIODELRT,但使用sysctl()系统调用更能发挥倾泻路由信息的手段。路由套接口的协议簇为AF_ROUTE,用作获取或设置路由表接口清单。
AF_UNSPEC(未说明协议簇),同用IPV4、IPV6用来获取和设置一些套接口层变量,如发送或者接受缓冲区的最大大小。
14. UDP是可用在任播,组播, 广播当中的。而TCP仅用于单播。
广播主要的用途是从本地子网中定位一个服务器,并据此服务器分配一个可用的IP,常用在ARP地址解析协议,DHCP动态主机控制协议,NTP网络时间协议,路由守护进程协议。
广播编程的时候,要明确的告诉当前套接口要进行广播发送,默认协议栈是不允许进行广播的,设置方法是通过setsockopt的SO_BROADCAST选项打开套接口的广播许可。UDP广播数据的接受recvfrom是阻塞的,使用定时退出往往会带来竞争条件,有三种手段可以针对这种alarm定时模式带来的竞争条件:
(1) pselect----让信号掩码设置,测试描述符,恢复信号掩码三个操作过程能够以原子的方式执行。
(2) siglongjump。这也是一种常用的避免竞争条件的方式,信号任时到来的时候能够恢复到信号之前的前一条汇编语句上而重新执行。
(3) 信号处理函数使用管道阻塞方式打开,select检测管道是否就绪,相当于设置一个可预见的条件来进行竞争条件避免。
15. 多播。
1) 多播地址与以太网地址的转换:
多播地址为D类地址,范围为224.0.0.0-239.255.255.255。多播转换的以太网地址头24bit为01:00:5e,23bit始终为0,低23bit为多播地址的低23bit。多播组ID概念是多播地址的低28bit,它的高5bit序列无效忽略,因此32个多播地址可以映射到同一个以太网地址,他们不是一对一映射的,这就引出了完备过滤和不完备过滤的概念
2) 完备过滤和不完备过滤的概念
不完备过滤是指在数据链路层,主机加入某个多播地址,也可以加入另一个多播地址,或者同一个多播地址不同端口,那么该主机可以接受这个多播数据,也可以接收另一个多播数据,这是最常见的情况。
完备过滤是在IP层,它通过IP层让发送来的多播数据能够区分到底是哪一个主机发来的,比较所有多播列表中的加入主机来确定,再交由UDP应用进程来处理相应的数据。
3) 广域网也是要有多播的,它通过MRP多播路由协议来实现分散在不同局域网段的主机形成多播,然而多播的IP在广域网是有数量局限的,频繁在广域网上指定不同的多播IP会带来请求上的问题,因此引出了源特定多播概念,它在设置多播地址之后并同时给出发送数据的源主机IP从而确定更小范围内的多播数据来源。
4) 多播编程当中,常用设置套接口的变量如下: IP_ADD_MEMBERSHIP,IP_DROP_MEMBERSHIP,IP_BLOCK_SOURCE,IP_UNBLOCK_SOURCE,
IP_ADD_SOURCE_MEMBERSHIP,IP_DROP_SOURCE_MEMBERSHIP,这六个变量用于控制接收;
IP_MULTICAST_IP,IP_MULTICAST_TTL,IP_MULTICAST_LOOP,这三个变量用于控制发送。
16. UDP代替TCP的编程要点:
1) 一些UDP程序需要获得套接口的接口信息,接口索引,宿IP地址等信息,那么就要使用recvmsg函数调用的变种dg_recv_send,使用套接口辅助数据的封装获得这些信息,关键的配置参数可以用这两个设置项获得:IP_RECVDSTADDR, IP_RECVIF。
2) 使用UDP代替TCP编程的关键是让UDP能够重传数据,UDP同样也有请求,响应ACK,数据发送,为了能稳健地保证数据能够正确到达宿端,就要设置RTO(retransmission TimeOut)
3) UDP不像TCP一样具有窗口流量控制,那么如何防止快速的发送淹没慢速的接收?一般是通过数据截断TRUNC来引起当前被截断数据的UDP超时重发。
4) UDP通常都是迭代的,但是迭代的东西总是慢速的,能不能让UDP能够像TCP一样并发执行,是提高UDP吞吐量的关键。他不能像TCP一样,使用fork来针对一对套接口来完成监听和连接,但UDP可以在某种应用当中如(TFTP,NFS(同时TCP UDP支持))可以采用这种类似的概念,只是方法不同而已:对于UDP的请求和确认,使用父进程来进行处理,fork()子进程,创建一个新的套接口并绑定bind一个临时的端口号,来针对数据的传送进行处理。
5) UDP协议不适合海量数据传送。
17.
1) 带外数据OOB是指能够在紧急的情况下通知对方进程的一种数据传送,它能够更快地及时反映对端的情况,比如是否崩溃,是否宕机,是否已经到达拥塞等等,常用的套接口设置为SOL_OOBINLINE,发送带外数据的时候send加上MSG_OOB标志位。
2) 心搏函数是封装带外数据的应用,SOL_KEEPLIVE保活探测分节同它有一样的作用,即检测对端网络的状态,但心搏函数能够提供更加短时间的试探来发现对端主机状况。
3) 套接口的线程编程能够极大的节约系统资源,较多考虑的是线程之间的同步问题。