最近自己在学习套接字,感觉原始套接字这块内容比较难以理解,但这部分的内容却十分有意思,所以自己对自己的学习到的知识点进行总结。
在学习int socket(int domain, int type, int protocol)时发现在函数的第二个参数位置type类型有3种。
SOCK_STREAM 字节流套接字
SOCK_DGRAM 数据报套接字
SOCK_RAM 原始套接字
字节流套接字提供有序,可靠,双向字节流的连接。
数据报套接字是在AF_INET域中通过UDP/IP连接实现,它提供的是一种无需连接的不可靠服务。
原始套接字与前面两种套接字不同。详细原因看后面的总结。
1.套接字的域
域指定套接字通信中使用的网络介质。最常见的套接字域是AF_INET,它是指Internet网络,许多Linux局域网使用的都是该网络,当然,因特网自身用的也是它。其底层的协议——网际协议(IP)只有一个地址族,它使用一种特定的方式来指定网络中的计算机,即IP地址。
2.原始套接字如下图中所示:在网际网协议族中的传输层中,有一个通道,使得网络应用可以绕过传输层直接使用ip层。原始套接字就是这样工作的。因此导致了原始套接字的独有的特性。
那么到底原始套接字与一般的套接字有什么不同呢?
总的来说就是以前由系统做的事情,现在由我们做。当我们创建一般套接字时,我们只是把我们发送的内容(buffer)传递给了系统,系统收到我们的数据后,会自动的调用相应的模块给数据加上TCP报头和IP报头,再发送出去。而当创建原始套接字时,就需要我们自己创建TCP,IP报文头部,系统仅仅是帮我们把数据发送出去。这样看来原始套接字只能接收ICMP, IGMP等数据报,而要传送TCP,UDP这样的数据报,需要我们自己创建报头。
协议栈的原始套接字可以分为两类:
(1)链路层原始套接字
用来接收和发送链路层的MAC帧,再发送时需要我们自己来构造和封装MAC首部。
构造链路层原始套接字
socket(AF_PACKET, SOCK_RAW, protocol);
其中protocol指定使用的协议。在/usr/include/linux/if_ether.h中对不同的协议有不同的声明。
#define ETH_P_IP 0x0800 /* Internet Protocol packet */
#define ETH_P_ARP 0x0806 /* Address Resolution packet */
#define ETH_P_RARP 0x8035 /* Reverse Addr Res packet */
#define ETH_P_ALL 0x0003 /* Every packet (be careful!!!) */
(2)网络层原始套接字
构造网络层原始套接字
socket(AF_INET, SOCK_RAW, protocol);
其中protocol指定使用的协议。在/usr/include/netinet/in.h中有声明。
enum
{
IPPROTO_IP = 0, /* Dummy protocol for TCP. */
#define IPPROTO_IP IPPROTO_IP
IPPROTO_HOPOPTS = 0, /* IPv6 Hop-by-Hop options. */
#define IPPROTO_HOPOPTS IPPROTO_HOPOPTS
IPPROTO_ICMP = 1, /* Internet Control Message Protocol. */
#define IPPROTO_ICMP IPPROTO_ICMP
IPPROTO_IGMP = 2, /* Internet Group Management Protocol. */
#define IPPROTO_IGMP IPPROTO_IGMP
IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94). */
#define IPPROTO_IPIP IPPROTO_IPIP
IPPROTO_TCP = 6, /* Transmission Control Protocol. */
#define IPPROTO_TCP IPPROTO_TCP
IPPROTO_EGP = 8, /* Exterior Gateway Protocol. */
#define IPPROTO_EGP IPPROTO_EGP
IPPROTO_PUP = 12, /* PUP protocol. */
#define IPPROTO_PUP IPPROTO_PUP
IPPROTO_UDP = 17, /* User Datagram Protocol. */
tcp报头占20个字节
struct tcphdr
{
u_int16_t source; //16位源端口
u_int16_t dest; //16位目的端口
u_int32_t seq; //32位序列号
u_int32_t ack_seq; //32位确认号
# if __BYTE_ORDER == __LITTLE_ENDIAN //小端模式 通常PC电脑都是小端模式
u_int16_t res1:4; //4位TCP偏移量
u_int16_t doff:4; //4位保留位
u_int16_t fin:1; //6个标志位
u_int16_t syn:1;
u_int16_t rst:1;
u_int16_t psh:1;
u_int16_t ack:1;
u_int16_t urg:1;
u_int16_t res2:2; //2位保留位
# elif __BYTE_ORDER == __BIG_ENDIAN //大端模式
u_int16_t doff:4;
u_int16_t res1:4;
u_int16_t res2:2;
u_int16_t urg:1;
u_int16_t ack:1;
u_int16_t psh:1;
u_int16_t rst:1;
u_int16_t syn:1;
u_int16_t fin:1;
# else
# error "Adjust your defines"
# endif
u_int16_t window; //窗口大小
u_int16_t check; //校验和
u_int16_t urg_ptr; //紧急位
};
# endif /* __FAVOR_BSD */
这里在构造TCP报头的时候,原始套接字提供了个有用的参数IP_HDRINCL,
setsocketopt(int socketfd, int level, int optname, const void *optval, socklen_t optlen);
通过setsocketopt()函数可以设置套接字的属性。当把第三个参数置为IP_HDRINCL时,我们可以从IP报文首部第一个字节开始依次构造整个IP报文的所有的选项,但是IP报文头的标识字段,和IP首部校验和由内核自己维护,不需要我们关心。如果不设置该选项,我们所构造的报文是从IP首部之后的第一个字节开始,IP首部由内核维护,首部协议字段设置成调用socket()函数时我们传递的第三个参数。
原始套接字的作用:
许多网络的诊断工具都是利用原始套接字来实现的,例如tcpdump抓包工具,ping检查网络连接工具,traceroute跟踪IP报文在网络中的路由经过。