网络编程中,一条全双工的连接可以想象成如下图示(由本地的ip+port和远端的ip+port标识一条连接):
Tip:SCTP(流控制传输协议),能够像TCP一样提供可靠的全双工数据传送。支持流量控制和消息排序。与TCP不同之处在于,TCP的连接只涉及两个IP地址之间的通信,而SCTP关联着两个系统之间的通信,可能不止一个连接,而且是面向消息
。
大端
小端
MSB:最高有效字节,例如数字13456的最高有效字节为1;因此不管字节序怎么变化,最高有效字节存储的数据都是一样的,不同的是内存地址的顺序,如上图。
域类型:
数据传输方式:
协议:
UNIX域套接字绑定的是文件(如果文件已经存在,则会绑定失败),其他域绑定的是“IP地址+port端口号”。
struct sockaddr_un{
sa_family_t sun_family;
char sun_path[104]; //以空字符结尾的文件名
}
connect函数
当在一个非阻塞的TCP套接字上调用connect时,connect将立即返回一个EINPROGRESS错误
(如果是同一台机器连接建立的非常迅速也可能会成功),不过已经发起的TCP三路握手会继续执行。后续可用select检测握手是成功还是失败。listen函数
isten函数的参数二表示的是三次握手未完成队列和已完成队列(未调用accept函数)之和的最大值。
accept函数
accept函数的第一个参数是监听套接字描述符
,其返回值是已完成连接套接字描述符
。值得注意的是这两个套接字都是绑定的同一个端口(这里应该是内核做了特殊处理,非直接bind)。
非阻塞accept可以解决的问题
:如果使用非阻塞模式,有连接到来,在select返回后,调用accept。如果select和accept之间间隔了一段时间,并且在此期间对端发来了rst,那么accept可能会导致永远阻塞。
在TCP连接中,主动关闭方会进入到一个TIME_WAIT状态,这个状态的持续时间一般是最长分解生命期(MSL)的两倍,即2MSL。状态存在的意义有两个理由:
(1)可靠的实现tcp全双工连接的终止。
(2)允许老的重复分节在网络中消逝(可能由于发生路由循环等故障,导致上一个连接的数据影响到本次连接)。
字节序(存储格式)
、C数据类型存储位数
以及机器的对齐限制
等体系结构保持一致。在传递文本串时,也要保证两端具有相同的字符集。否则,会出现序列化的错误导致获取的数据不准确。UDP异步错误
,当一个UDP客户端使用sendto给一个未启动的服务器发送数据后,紧接着使用recvfrom接受数据,recvfrom将会一直阻塞。其原因是,该错误是有sendto引起的,但是sendto却成功返回了(因为UDP输出操作成功返回仅仅表示输出队列中具有存放该输出数据包的空间)。真正的错误直到实际发送才会返回。断开UDP套接字的连接
也需要使用connect函数UDP套接字不存在真正意义上的发送缓冲区。
accept返回前连接终止
在三路握手完成建立连接后,客户端TCP发了一个RST(复位),在服务器还未调用accept时RST到达。此时,服务器的accept会返回,并设置errno为ECONNABORTED。
客户端终止
客户端关闭套接字描述符,向服务器发送FIN,服务器则以ACK响应,此时服务器处理CLOSE_WAIT状态,客户端处理FIN_WAIT_2状态;服务器进行关闭操作(当收到FIN后,服务器继续对套接字读会返回0,表示读结束,此时服务器应做关闭套接字操作),发送FIN给客户端,同时客户端会返回ACK,并处于TIME_WAIT状态。
服务器终止
当服务器进程终止时,也会向客户端发送一个FIN,客户端则会响应一个ACK。但是一般客户端都是先向服务器写数据,然后再向其读数据(我们就认为这是客户端和服务端的一个区别,方便对比)。此时,客户端会向服务器发送一个数据(TCP允许这么做),当服务器接收到这个数据时,会响应一个RST(因为进程已经终止);此时,如果客户端继续向服务器写数据,内核就会想向客户端进程发送一个SIGPIPE信息;当然,客户端也可以读服务器数据,正如之前服务器一样,读会返回0,然后关闭描述符。
服务器崩溃
当服务器主机崩溃时(而不是执行命令退出进程或者关机),客户端此时向服务器发送数据会触发TCP重传机制,一般需要等待数分钟才会返回。返回的错误码为ETIMEDUT或EHOSTUNREACH或ENETUNREACH。
服务器崩溃后重启,它的TCP会丢失崩溃前的所有连接信息。当客户端向服务器发送数据时,服务器会响应一个RST,此时客户端如果去读套接字会返回一个ECONNRESET错误。
IO复用技术和标准IO库同时使用时的注意点
stdio库带有缓冲区,例如,文件有数据可读时(select准备就绪),使用stdio读取数据,文件中有2条数据,2条数据都已经读到了stdio的缓冲区,但是使用时只用到了第一条数据,用完之后继续使用select去等待数据可读,这时select不会管缓冲区中是不是还有数据,而是继续去等待数据可读(因为之前数据都读出来了放在缓冲区了,这里可能会一直阻塞)。其原因是,select是从read系统调用的角度去确定数据是否可读,而不会去管stdio中是否用了缓冲区(例子可能不恰当,主要是理解select不会去考虑IO函数是否有缓存区的存在
)。
半关闭问题
shutdown
函数可以不用管套接字的引用计数直接激发正常连接中止序列。
SHUT_RD,关闭套接字的读,接受缓冲区中的现有数据会被丢弃,来自对端的任何数据都被确认,然后丢弃。
SHUT_WR,关闭套接字的写(半关闭),当前留在发送缓冲区的数据都将被发送掉,后跟TCP的正常连接终止序列
。
缓冲区问题
各种关闭情况下读写缓冲区的数据处理(todo…)
close函数设置选项
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);
int ioctl(int fd , int request, ... /* void* */);
和网络相关的请求(request)可大致划分为6类:
总结:
到这里总共学习了三种可以设置套接字属性的函数,分别是:fcntl、setsockopt、ioctl。
服务端为ipv6监听套接字(双协议栈主机,绑定通配地址
),客户端使用ipv4数据报(因为是双协议栈主机,所以主机也有ipv4地址),服务端会将数据报中的ipv4地址映射为ipv6地址(下图虚线)。反之,则不成立,因为ipv6地址没法映射为ipv4。
ipv6客户端(双栈主机)想要和监听ipv4套接字的服务端连接,需要通过主机地址转换函数
(例getaddrinfo),获取服务器ipv4地址到ipv6的一个映射,然后调用connect,内核检测到这个映射后会改为发送ipv4数据报(下图虚线)。
注:支持双栈协议的主机一般都会有ipv4的地址和ipv6的地址。
信号可以中断系统调用的特性
。辅助数据可通过调用sendmsg和recvmsg
时,使用msghdr结构中的msg_control和msg_controllen这两个成员发送和接受。其用途有:
辅助数据可由一个或多个辅助数据对象构成,每个对象的定义如下:
注意:对于TCP套接字来说,不建议使用sendmsg()和recvmsg()函数发送或接收辅助数据
。因为在TCP协议中,辅助数据的传输是不可靠的,可能会导致一些问题。而UDP套接字对于辅助数据的传输没有任何问题
。
带外数据分为三部分:
(1)URG标志紧急模式,发送端发送带外数据进入紧急模式,并且会立即通知到接收端,即使接收端因为流量控制而停止接收数据了,TCP仍会发送本通知。
(2)紧急指针带外标记,带外数据相对于发送端其他数据的发送位置。在使用系统调用读取数据时,碰到这个标记时会返回。
(3)带外数据本身。
与带外数据概念相关问题:
(1)每个连接只有一个TCP紧急指针。
(2)每个连接只有一个带外标记。
(3)每个连接只有一个单字节的带外缓冲区(这个缓冲区只有在数据离线读取时才需要考虑)。
带外数据获取:
套接字API把TCP的紧急模式映射成所谓的带外数据,发送进程通过指定MSG_OOB标志调用send函数让发送端进入紧急模式。接收端TCP收到新的紧急指针后,或通过SIGURG信号处理,或由select返回套接字有异常条件待处理。
默认情况下,接收端TCP会把带外数据从普通数据流中取出存放到自己的单字节带外缓冲区
,供接收进程通过指定MSG_OOB标志调用recv获取。
接收进程也可以开启SO_OOBINLINE套接字选项,这种情况下,带外字节被留在普通数据流中。
无论使用上述那种方法,套接字层都在数据流中维护一个带外标记,并且不允许单个类read操作越过这个标记(到达这个标记函数会返回)。
注意
,TCP没有真正的带外数据,它是通过紧急模式和紧急指针来实现带外数据的效果。因此所有的数据(带外数据本身)仍然受到TCP的流量控制
。
创建一个路由套接字后,进程可以通过该套接字向内核发送命令;通过读该套接字从内核接收消息(需要root权限)。
sysctl可以用来检查路由表和接口列表(无需root权限)。
int sysctl(int *name , u_int namelen, void *oldp, size_t *oldlenp, void *newp, size_t newlen);
上述各种函数的总结:
其他主机都会收到该广播数据,且沿着协议栈一路向上
,有监听广播目的端口的应用程序则会处理,没有的则会丢弃。这是使用广播的一个根本缺陷。单播地址标识单个IP接口,广播地址标识所有IP接口,多播地址标识一组IP接口。广播只能用于局域网,而多播既可以用于局域网,也可以用于广域网。
D类地址的低序28位构成多播组ID
,整个32位地址称为组地址。TCP的优势:
(1)传输确认,丢失分组重传,重复分组检测,传输分组排序。
(2)滑动窗口流量控制。
(3)慢启动和拥塞阻塞。
UDP优势:
(1)广播和组播必须使用UDP。
(2)UDP开销更小,没有连接的建立和拆除。
为了增加UDP的可靠性,可以在应用程序中增加以下两个特性:
(1)超时和重传,处理丢失数据报。
(2)数据报序列号,验证请求和应答是否匹配。
一个套接字需要使用信号驱动式IO,要执行以下三个步骤:
(1)建立SIGIO信号的信号处理函数。
(2)设置该套接字的属主,可以使用fcntl的F_SETOWN命令来设置。
(3)开启该套接字的信号驱动式IO,可以使用fcntl的F_SETFL命令打开O_ASYNC标志完成。
1.原始套接字提供了普通TCP和UDP套接字所不能及的3个能力:
(1)通过原始套接字,进程可以读写ICMPv4、IGMPv4和ICMPv6等分组。
(2)通过原始套接字,进程可以读写内核不处理其协议字段的IPv4数据报(自定义协议字段)。
(3)通过原始套接字,可以使用IP_HDRINCL套接字选项自行构造IP首部(IPV4)。如果IP_HDRINCL选项未开启,进程构造的数据是从IP首部之后的第一个字节开始
;如果选项开启,进程构造的数据是IP首部的第一个字节
。
总的来说,原始套接字可以接收处理除了TCP和UDP的其他协议数据分组。其原理是原始套接字有自行处理IP数据报的能力。
2.原始套接字的创建:
//protocol为0或者形如IPPROTO_xxx的常值,如IPPROTO_IGMP,内核通过其过滤是否将数据报传递到该套接字
int rawsock = socket(AF_INET, SOCK_RAW, x/*protocol*/);
只有超级用户才有权创建原始套接字,这样做的目的是防止普通用户往网络写它们自行构造的IP数据报。
3.原始套接字输出:
(1)和普通套接字一样,可以通过sendto、sendmsg并指定ip地址完成或者,套接字已连接使用write、writev、send完成。
(2)通过IP_HDRINCL选项来确定发送的数据是否包含IP首部。
4.原始套接字输入:
(1)接收到的UDP和TCP分组绝不会传递给原始套接字。
(2)大多数ICMP分组在内核处理完其中的ICMP消息后传递到原始套接字。
(3)内核不认识其协议字段的所有IP数据报传递到原始套接字。
5.内核匹配数据报该递送到那个套接字:
(1)如果创建原始套接字时指定了非0协议参数(socket参数3),那么接收到的数据报协议字段必须匹配该值。
(2)原始套接字如果bind了ip地址,则数据报目的地址必须匹配绑定的ip。
(3)原始套接字如果connect了某个外地ip地址,则数据报的源IP地址必须匹配这个连接的对端地址。
(4)如果一个原始套接字是以0值协议创建,并且没有调用bind和connect,则该套接字可以接收到内核收到的所有原始数据报的副本。
注意
:当内核往进程递送一个原始数据报时,如果是ipv4套接字,则传递到该套接字的数据包含完整的IP首部。如果是ipv6套接字,则扣除了ipv6首部和所有扩展首部。
6.ICMP消息处理
原始套接字一个重要的作用是可以处理UDP套接字接收异步ICMP错误问题。
1.Unix中访问数据链路层的3个常用方法:BSD的分组过滤器BPF、SVR4的DLPI和Linux的SOCK_PACKET接口。
(1)BSD:BPF过滤器
在支持BPF的系统上,每个数据链路驱动程序都在发送一个分组之前或在接收一个分组之后调用BPF。
注意:应用程序也可以往BPF中写数据,使得数据分组通过数据链路往外发送。但是大多数应用程序仅仅是读BPF。
为了使用BPF,首先需要打开BPF设备(/dev/bpfn),然后通过ioctl命令来设置该装备的特征,最后使用writer、read执行IO。
(2)SVR4:DLPI,数据链路提供者接口
(3)Linux:SOCK_PACKET和PF_PACKET
Linux先后有两种从数据链路层接收分组的方法:
a.较久的方法是创建类型为SOCK_PACKET的套接字(可用面广,但缺乏灵活性)
socket(AF_INET, SOCK_PACKET, htons(ETH_P_ALL)); //接收所有帧
socket(AF_INET, SOCK_PACKET, htons(ETH_P_IP)); //只捕获IPv4帧
b.较新的方法是创建协议族为PF_PACKET的套接字(引入了更多的过滤和性能特性)
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); //接收所有帧
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)); //只捕获IPv4帧
2.数据链路层常用的函数库
(1)libpcap:分组捕获函数库
libpcap是访问操作系统所提供的分组捕获机制的分组捕获函数库,它是与实现无关的(跨平台使用)。
(2)libnet:分组构造与输出函数库
libnet函数库提供构造任意协议的分组并将其输出到网络中的接口。它以与实现无关的方式提供原始套接字访问方式和数据链路访问方式。
3.混杂模式
网络接口进入混杂模式可以让应用程序拥有监视本地电缆流通的所有分组,而不仅仅是以程序运行所在主机为目的地的分组。
如下图所示,展示了网络协议在流框架中的实现机制。例如,传输层提供了TPI(传输层提供者接口,它包括了交互消息的结构和每个消息执行的操作),应用进程中的socket使用TPI和传输层交互(请求-应答消息)。
一个使用TPI的例子是:应用进程向提供者(tcp驱动模块)发出一个绑定某个地址的请求,提供者则发回一个响应,成功或者出错。一些事件在提供者异步发生(比如对服务器的连接请求到达),它们导致沿着流向上发送消息或信号。
/*
服务器名字和地址转换(域名系统、getaddrinfo/gethostbyname/gethostbyaddr...);
获取系统网络接口等内核数据(路由套接字、sysctl、ioctl);
网络编程(TCP/UDP/Unix域套接字/原始套接字/数据链路接口);
套接字选项操作(套接字属性、关联辅助数据、设置IP数据报等);
ipv4和ipv6互操作性;
广播和多播;
带外数据;
*/