linux网络编程(十一)UNIX域套接字与数据链路层

一、UNIX 域
1. UNIX 域函数
UNIX域的协议族是在同一台主机上的客户/服务器通信时使用的一种方法。 相对其他方法(例如进程间通信的管道),它在形式上与传统套接字API的调用方法相同。UNIX域有两种类型的套接字:字节流套接字和数据报套接字,字节流套接字类似于TCP,数据报套接字类似于UDP。UNIX域的套接字有如下的特点值得注意:
  • UNIX 域套接字与TCP套接字相比较,在同一台主机的传输速度前者是后者的两倍。
  • UNIX域套接字可以在同一台主机上各进程之间传递描述符。
  • UNIX域套接字与传统套接字的区别是用路径名来表示协议族的描述。
UNIX域函数的地址结构
UNIX域的地址结构在文件<sys/un.h>中定义,结构的原型如下:
  • UNIX域地址结构成员变量sun_family的值是AF_UNIX或者AF_LOCAL。
  • sun_path是一个路径名,此路径名的属性为0777,可以进行读写等操作。
2. 套接字函数
UNIX域的套接字函数和以太网套接字(AF_INET)的函数相同,但是当用于UNIX域套接字时,套接字函数有一些差别和限制,主要有如下几条:
  • 使用函数bind()进行套接字和地址的绑定的时候,地址结构中的路径名和路径名所表示的文件的默认访问权限为 0777,即用户、 用户所属的组和其他组的用户都能读、 写和执行。
  • 结构sum_path中的路径名必须是一个绝对路径,不能是相对路径。
  • 函数 connect()使用的路径名必须是一个绑定在某个己打开的UNIX域套接字上的路径名,而且套接字的类型也必须一致。下列情况将出错:
  1. 该路径名存在但不是一个套接字;
  2. 路径名存在且是一个套接口,但没有与该路径名相关联的打开的描述字;
  3. 路径名存在且是一个打开的套接字,但类型不符。
  • 用函数 connect() 连接UNIX域套接字时的权限检查和用函数open()以只写方式访问路径名完全相同。
  • UNIX域字节流套接字和TCP套接字类似:它们都为进程提供一个没有记录边界的字节流接口。
  • 如果UNIX域字节流套接字的 connect() 函数发现监听套接字的队列己满,会立刻返回一个ECONNREFUSED错误。这和TCP有所不同:如果监听套接字的队列己满,它将忽略到来的SYN,TCP连接的发起方会接着发送几次SYN重试。
  • UNIX域数据报套接字和UDP套接字类似: 它们都提供一个保留记录边界的不可靠的数据服务。
  • 与UDP套接字不同的是,在未绑定的UNIX域套接字上发送数据报不会给它捆绑一个路径名。这意味着,数据报发送者除非绑定一个路径名,否则接收者无法发
  • 回应答数据报。 同样,与TCP和UDP不同的是,给UNIX域数据报套接字调用 connect() 不会捆绑一个路径名。
3. 传递文件描述符
在进程之间经常遇到需要在各进程之间传递文件描述符的情况,例如有一种设备它在加电期间只能打开一次,如果关闭后再次打开就会发生错误。这时就需要有一个调度程序,它调度多个相同设备,当有客户端需要此类型的设备时会向 它发送一个请求,服务器会把某个设备的描述符给客户端。但是,由于不同进程之间的文件描述符所表示的对象是不同的,这需要一种特殊的机制来实现上述的要求。
 
Linux系统中提供了一种特殊的方法,可以从一个进程中将一个已经打开的文件描述符传递给其他的任何进程。 其基本过程如下所述。
  1. 创建一个字节流或者数据报的UNIX域套接字。
    • 如果目标是fork()一个子进程,让子进程打开描述符并将它返回给父进程,那么父进程可以用socketpair()创建一个流管道,用它来传递描述字。
    • 如果进程之间没有亲缘关系,那么服务器必须创建一个UNIX域字节流套接字,绑定一个路径名,让客户连接到这个套接字。 然后客户端可以向服务器发送一个请求以打开某个描述字,服务器将描述符通过UNIX域套接字传回。在客户端和服务器之间也可以使用UNIX数据报套接字,但这样做没有什么好处,而且数据 报存在丢失的可能性。
  1. 进程可以用任何返回描述符的UNIX函数打开,例如函数open()、pipe()、mkfifo()、 socket()或者accept()。可以在进程间传递任何类型的描述符。
  2. 发送进程建立一个 msghdr结构,其中包含要传递的描述符。在POSIX中说明该描述符作为辅助数据发送,但老的实现使用msg_accright成员。发送进程调用sendmsg()通过第一步得到的UNIX域套接字发出套接字。这时这个描述符是在飞行中的。即在发送进程调用sendmsg()之后、 在接受进程调用recvmsg()之前将描述符关闭,它仍会为接收进程保持打开状态。描述符的发送导致它的访问统计数加1。
  3. 接收进程调用recvmsg()在UNIX域套接字上接收套接字。通常接收进程收到的描述符的编号和发送进程中的描述符的编号不同,但这没有问题。传递描述符不是传递描述符的编号,而是在接收进程中建立一个新的描述符,指向内核的文件表中与发送进程发送的描述符相同的项。
 
4. socketpair() 函数
socketpair()函数建立一对匿名的己经连接的套接字,其特性由协议族d、类型type、 协议protocol决定,建立的两个套接字描述符会放在sv[0]和 sv[1]中。
socketpair()函数的原型如下,第1个参数d,表示协议族,只能为AF_LOCAL或者 AF_UNIX:第2个参数type,表示类型,只能为0。第3个参数 protocol, 表示协议,可以是SOCK_STREAM或者SOCK_DGRAM。 用SOCK_STREAM建立的套接字对是管道流,与一般的管道相区别的是,套接字对建立的通道是双向的,即每端都可以进行读写。参数sv,用于保存建立的套接字对。

 

 socketpair() 函数的返回值为0时表示调用成功,为-1时表示发生了错误,错误值在变量errno中,errno的含义如 表11.3 所示。

socketpair()函数建立两个套接字文件描述符sv[0]和sv[1],如图11.1所示:
socketpair()函数建立的描述符可以使用类似管道的处理方法在两个进程之间通信。使用函数socketpair()建立套接字描述符后,在一个进程中关闭其中的一个,在另一个进程中关闭另一个,如图11.2所示。调用函数socketpair()后,fork进程在进程A中关闭sv[0],在进程B中关闭 sv[1],则会形成图中所示的状况。

 

 

二、数据链路层的访问
在Linux 下数据链路层的访问通常是通过编写内核驱动程序来实现的,在应用层使用SOCK_PACKET类型的协议族可以实现部分功能。
 
1. SOCK_PACKET 类型
建立套接字的时候选择SOCK_PACKET类型,内核将不对网络数据进行处理而直接交给用户,数据直接从网卡的协议核交给用户。建立一个SOCK_PACKET类型的套接字使用如下方式:
socket(AF_INET, SOCK_PACKET, htons(Ox0003));
其中AF_INET表示因特网协议族,SOCKPACKET表示截取数据帧的层次在物理层,网络协议核对数据不做处理。值Ox0003表示截取的数据帧的类型为不确定,处理所有的包。
使用 SOCK_PACKET进行程序设计的时候,需要注意的主要方面包括协议族选择、获取原始包、 定位IP包、 定位TCP包、 定位UDP包、 定位应用层数据几个部分。
 
2. 设置套接口以捕获链路帧的编程方法
在 Linux 下编写网络监听程序,比较简单的方法是在超级用户模式下,利用类型为SOCK_PACKET的套接口(用 socket() 函数创建)来捕获链路帧数据。
要监听其他网卡的数据,需要将本地的网卡设置为"混杂"模式;当然还需要一个都连接于同一 HUB的局域网或者具有"镜像"功能的交换机才可以,否则,只能接收到其他主机的广播包。
使用了ioctl()的SIOCGIFFLAGS和SIOCSIFFLAGS命令,用来取出和写入网络接口的标志设置。注意,在修改网络接口标志的时候,务必要先将之前的标志取出,与想设置的位进行 "位或" 计算后再写入;不要直接将设置的位值写入,因为直接写入会覆盖之前的设置,造成网络接口混乱。 遵循如下步骤:
  • 取出标志位。
  • 目标标志位=取出的标志位|设置的标志位。
  • 写入目标标志位。
3. 从套接口读取链路帧的编程方法
以太网的数据结构如图11.10所示,总长度最大为1518字节,最小为64字节,其中目标地址的MAC为6字节,源地址MAC为6字节,协议类型为2字节,含有46~1500字节的数据,尾部为4个字节的CRC校验和。以太网的CRC校验和一般由硬件自动设置 或者剥离,应用层不用考虑。

 

 以太网头部结构定义为如下形式:

 

 套接字文件描述符建立后,就可以从此描述符中读取数据,数据的格式为上述的以太网数据,即以太网帧。套接口建立以后,就可以从中循环读取捕获的链路层以太网帧。 要建立一个大小为 ETH_FRAME_LEN 的缓冲区,并将以太网的头部指向此缓冲区,接收数据以后,缓冲区 ef 与以太网头部的对应关系如图 11.11 所示。

 

 因此,要获得以太网帧的目的 MAC 地址、 源 MAC 地址和协议的类型, 可以通过p_ethhdr->h_dest、p_ethhdr->h_source 和p_ethhdr->h_proto 获得。

 

4. 定位IP包头的编程方法
获得以太网帧后,当协议为 0x0800 时,其负载部分为 IP 协议。 IP 议的数据结构如:

 

 IP头部的数据结构定义在头文件<netinet/ip.h>中,代码如下:

 

 若捕获的以太帧中h_proto的取值为0x0800,将类型为iphdr的结构指针指向帧头后面载荷数据的起始位置,则可以得到IP数据包的报头部分。通过saddr和daddr可以得到 IP 报文的源IP地址和目的IP地址。

 

5. 定位TCP报头编程方法
TCP的数据结构如图11.13所示。

 

 对应的数据结构在头文件<netinet/tcp.h>中定义,代码如下:

 

 

 对于 TCP 协议,其 IP 头部的 protocol 的值应该为 6,通过计算 IP 头部的长度可以得到 TCP 头部的地址,即 TCP 的头部为 IP 头部偏移 ihl*4。 TCP 的源端口和目的端口可以通过成员 source 和 dest 来获得。

 

6. 定位UDP报头的编程方法
UDP 的数据结构如图 11.14 所示。

 

 UDP 的头部数据结构在文件<netinet/udp.h>中定义,代码如下:

 

 对于UDP协议,其IP头部的protocol的值为17,通过计算IP头部的长度可以得到 UDP头部的地址,即UDP的头部为IP头部偏移ihl*4。UDP的源端口和目的端口可以通过成员 source 和 dest 来获得。头部数据结构的布局如图 11.15 所示:

 

7. 定位应用层报文数据的编程方法
定位了 UDP 和 TCP 头部地址后,其中的数据部分为应用层报文数据。 根据 TCP 和 UDP 的协议获得应用程序指针的代码如下:

 

 ## 协议名称数据表

 

 

posted @ 2020-07-14 18:16  chenjy2019  阅读(495)  评论(0)    收藏  举报