网络编程
- 网络协议模型
- 网络中的一条连接
- 套接字编程
- 字节序
- 套接字地址结构
- socket创建套接字
- UNIX域套接字
- tcp套接字函数注意点
- TCP绑定端口问题
- TIME_WAIT状态
- 使用TCP编程注意点
- 使用UDP编程注意点
- 网络中数据大小的限制
- 客服端-服务器交互问题
- 网络数据读写问题
- 常见套接字选项
- 设置套接字选项的函数(*)
- ioctl函数(*)
- IPV4和IPV6的互操作性
- 5组IO函数
- 设置套接字超时的三种方式
- 辅助数据
- 带外数据(TCP)
- 路由套接字(*)
- sysctl函数(*)
- 广播
- 多播
- UDP和TCP的选择
- 信号驱动式IO
- 原始套接字
- 数据链路访问
- 客户/服务器程序设计范式
- 流
- 随便记点东西
网络协议模型
网络中的一条连接
网络编程中,一条全双工的连接可以想象成如下图示(由本地的ip+port和远端的ip+port标识一条连接):
Tip:SCTP(流控制传输协议),能够像TCP一样提供可靠的全双工数据传送。支持流量控制和消息排序。与TCP不同之处在于,TCP的连接只涉及两个IP地址之间的通信,而SCTP关联着两个系统之间的通信,可能不止一个连接,而且是面向消息
。
套接字编程
字节序
大端
小端
MSB:最高有效字节,例如数字13456的最高有效字节为1;因此不管字节序怎么变化,最高有效字节存储的数据都是一样的,不同的是内存地址的顺序,如上图。
套接字地址结构
见章节-sockaddr 数据结构
socket创建套接字
域类型:
数据传输方式:
协议:
UNIX域套接字
UNIX域套接字绑定的是文件(如果文件已经存在,则会绑定失败),其他域绑定的是“IP地址+port端口号”。
struct sockaddr_un{sa_family_t sun_family;char sun_path[104]; //以空字符结尾的文件名
}
- 在一个未绑定路径名的域套接字上发送数据报不会自动给这个套接字绑定一个路径名,这点不同于UDP套接字。
- 域套接字可以在两个进程间传递描述符(不限类型)。
- 域套接字可以在两个进程间传递用户凭证(权限)。
tcp套接字函数注意点
-
connect函数
- 若connect失败则该套接字不可再用,不能对这样的套接字继续调用connect函数。在每次connect失败后,都必须close当前套接字并重新调用socket。
当在一个非阻塞的TCP套接字上调用connect时,connect将立即返回一个EINPROGRESS错误
(如果是同一台机器连接建立的非常迅速也可能会成功),不过已经发起的TCP三路握手会继续执行。后续可用select检测握手是成功还是失败。
-
listen函数
isten函数的参数二表示的是三次握手未完成队列和已完成队列(未调用accept函数)之和的最大值。 -
accept函数
accept函数的第一个参数是监听套接字描述符
,其返回值是已完成连接套接字描述符
。值得注意的是这两个套接字都是绑定的同一个端口(这里应该是内核做了特殊处理,非直接bind)。
非阻塞accept可以解决的问题
:如果使用非阻塞模式,有连接到来,在select返回后,调用accept。如果select和accept之间间隔了一段时间,并且在此期间对端发来了rst,那么accept可能会导致永远阻塞。
TCP绑定端口问题
- 一个套接字只能绑定一个端口(多次绑定会失败)。
- 同一个端口可以被多个套接字绑定(使用SO_REUSEADDR选项,并且IP地址不能相同)。
- 同一个ip和同一个的端口无法被多个套接字绑定。
TIME_WAIT状态
在TCP连接中,主动关闭方会进入到一个TIME_WAIT状态,这个状态的持续时间一般是最长分解生命期(MSL)的两倍,即2MSL。状态存在的意义有两个理由:
(1)可靠的实现tcp全双工连接的终止。
(2)允许老的重复分节在网络中消逝(可能由于发生路由循环等故障,导致上一个连接的数据影响到本次连接)。
使用TCP编程注意点
- Tcp是面向字节流的(没有记录边界),因此每次使用read获取数据时,可能会一次性获取所有数据,也可能只会获取到一部分数据。在从tcp套接字中读取数据时,总是需要把read编写在某个循环中,当read返回0(结束)或者负值(出错)时终止循环。
- 在两个主机之间传输二进制结构时(例如结构体),需要保证两端的
字节序(存储格式)
、C数据类型存储位数
以及机器的对齐限制
等体系结构保持一致。在传递文本串时,也要保证两端具有相同的字符集。否则,会出现序列化的错误导致获取的数据不准确。 - tcp套接字也可以用sendto函数和recvfrom函数,但没必要这么做。
使用UDP编程注意点
- UDP的套接字也可以使用bind的函数绑定端口,但一般不会这么做。
UDP异步错误
,当一个UDP客户端使用sendto给一个未启动的服务器发送数据后,紧接着使用recvfrom接受数据,recvfrom将会一直阻塞。其原因是,该错误是有sendto引起的,但是sendto却成功返回了(因为UDP输出操作成功返回仅仅表示输出队列中具有存放该输出数据包的空间)。真正的错误直到实际发送才会返回。- UDP套接字也可以使用connect函数,但是没有三次握手的过程,对端也无须做任何监听操作,并且在调用connect时不会给对端主机发送任何信息,它完全是一个本地操作,仅仅保留对端的ip和端口号。但可以检查是否有立即可知的错误(例如上一条中的异步错误)。在使用了connect后会发生如下改变(UDP套接字也有了一对一的关系):
- 不需要强制使用sendto函数(不能指定目的地址和端口号),改用write和send。
- 不需要强制使用recvfrom函数(对端的ip地址和端口号已知)。
断开UDP套接字的连接
也需要使用connect函数
UDP套接字不存在真正意义上的发送缓冲区。
网络中数据大小的限制
- ipv4数据报最大大小是65535字节(包括ipv4首部);ipv6数据报最大大小是65575(包括40字节的ipv6首部);
- MTU,最大传输单元。以太网的最大传输单元通常为1500字节(ipv4要求最小MTU为48字节,ipv6要求最小是1280字节)。如果传输的数据报大小超过了MTU,ip层会对数据报进行分片。
- TCP有一个MSS(最大分节大小),用于向对端TCP通告对端每个分节中最大能发送的TCP数据量。
客服端-服务器交互问题
-
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…)
常见套接字选项
- SO_KEEPALVE,保活选项
- SO_LINGER,
close函数设置选项
- SO_REVBUF和SO_SNDBUF,接受发送缓冲区设置选项
- SO_RCVLOWAT和SO_SNDLOWAT,接受发送低水位设置选项
- SO_RCVTIMEO和SO_SNDTIMEO,设置套接字的接受和发送超时值
- SO_REUSEADDR,端口复用选项
- SO_BROADCAST,设置进程发送广播消息的能力
- SO_DONTROUTE,设置外出分组绕过底层协议的路由机制
- SO_OOBINLINE,设置带外数据,使其留在输入队列中
- TCP_MAXSEG,设置TCP连接的最大分节大小(MSS)
- TCP_NODELAY,设置Nagle算法
设置套接字选项的函数(*)
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);
ioctl函数(*)
int ioctl(int fd , int request, ... /* void* */);
和网络相关的请求(request)可大致划分为6类:
- 套接字操作(是否位于带外标记等等);
- 文件操作(设置或清除非阻塞标志等等);
- 网络接口操作(获取网络接口列表,获取广播地址等);
- ARP高速缓存操作(创建、修改、获取或删除arp表项);
- 路由表操作(增加或删除路由表项);
- 流系统;
总结:
到这里总共学习了三种可以设置套接字属性的函数,分别是:fcntl、setsockopt、ioctl。
IPV4和IPV6的互操作性
-
服务端为ipv6监听套接字(双协议栈主机,
绑定通配地址
),客户端使用ipv4数据报(因为是双协议栈主机,所以主机也有ipv4地址),服务端会将数据报中的ipv4地址映射为ipv6地址(下图虚线)。反之,则不成立,因为ipv6地址没法映射为ipv4。
-
ipv6客户端(双栈主机)想要和监听ipv4套接字的服务端连接,需要通过
主机地址转换函数
(例getaddrinfo),获取服务器ipv4地址到ipv6的一个映射,然后调用connect,内核检测到这个映射后会改为发送ipv4数据报(下图虚线)。
注:支持双栈协议的主机一般都会有ipv4的地址和ipv6的地址。
5组IO函数
设置套接字超时的三种方式
- 调用alarm定时函数,利用
信号可以中断系统调用的特性
。 - select设置超时等待参数。
- 使用SO_RCVTIMEO和SO_SNDTIMEO套接字选项
辅助数据
辅助数据可通过调用sendmsg和recvmsg
时,使用msghdr结构中的msg_control和msg_controllen这两个成员发送和接受。其用途有:
辅助数据可由一个或多个辅助数据对象构成,每个对象的定义如下:
注意:对于TCP套接字来说,不建议使用sendmsg()和recvmsg()函数发送或接收辅助数据
。因为在TCP协议中,辅助数据的传输是不可靠的,可能会导致一些问题。而UDP套接字对于辅助数据的传输没有任何问题
。
带外数据(TCP)
带外数据分为三部分:
(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函数(*)
sysctl可以用来检查路由表和接口列表(无需root权限)。
int sysctl(int *name , u_int namelen, void *oldp, size_t *oldlenp, void *newp, size_t newlen);
上述各种函数的总结:
广播
- 广播地址:某个子网的所有IP地址,例如,{子网id, -1}(xxx.xxx.xxx.255)、{-1, -1}(255.255.255.255)。
- 广播只能使用UDP套接字或者原始套接字。
- 当一个子网中某台主机发送广播数据报时,子网中的
其他主机都会收到该广播数据,且沿着协议栈一路向上
,有监听广播目的端口的应用程序则会处理,没有的则会丢弃。这是使用广播的一个根本缺陷。 - 当套接字需要使用广播时,需要显示的设置SO_BROADCAST套接字选项。
多播
单播地址标识单个IP接口,广播地址标识所有IP接口,多播地址标识一组IP接口。广播只能用于局域网,而多播既可以用于局域网,也可以用于广域网。
- 多播地址:一组IP地址,例如,IPV4的D类地址(从224.0.0.0到239.255.255.255),
D类地址的低序28位构成多播组ID
,整个32位地址称为组地址。 - 特殊的多播地址:
- ipv4,224.0.0.1,主机组(所有节点组,子网中所有具备多播能力的节点都需要加入改组);224.0.0.2,路由器组(子网中所有多播路由器需要加入改组)。介于224.0.0.0到224.0.0.255之间的地址是链路局部的多播地址。多播路由器从不转发以这些地址为目的地址的数据报。
- ipv6,ff01::1和ff02::1是所有节点组;ff01::2、ff02::2和ff05::2,路由器组。
UDP和TCP的选择
TCP的优势:
(1)传输确认,丢失分组重传,重复分组检测,传输分组排序。
(2)滑动窗口流量控制。
(3)慢启动和拥塞阻塞。
UDP优势:
(1)广播和组播必须使用UDP。
(2)UDP开销更小,没有连接的建立和拆除。
为了增加UDP的可靠性,可以在应用程序中增加以下两个特性:
(1)超时和重传,处理丢失数据报。
(2)数据报序列号,验证请求和应答是否匹配。
信号驱动式IO
一个套接字需要使用信号驱动式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互操作性;
广播和多播;
带外数据;
*/