Linux Socket 原始套接字编程
Posted 痞子辉
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux Socket 原始套接字编程相关的知识,希望对你有一定的参考价值。
对于linux网络编程来说,可以简单的分为标准套接字编程和原始套接字编程,标准套接字主要就是应用层数据的传输,原始套接字则是可以获得不止是应用层的其他层不同协议的数据。与标准套接字相区别的主要是要开发之自己构建协议头。对于原始套接字编程有些细节性的东西还是需要注意的。
1. 原始套接字创建
原始套接字的编程和udp网络编程的流程有点类似,但是原始套接字编程中不需要bind操作,因为在数据接收和发送过程中使用sendto和recvfrom函数实现数据的接收和发送。不过不是说原始套接字不能使用bind操作,如果在程序设计中使用了bind,则在数据接收和发送过程中就需要使用send和recv函数来实现。
原始套接字的创建和TCP、UDP编程一样使用socket函数来实现,只不过使用的协议族、套接字类型和协议类型不同而已。创建socket时,第一个参数同样是AF_INET,第二个参数设置为SOCK_RAW,第三个参数是协议类型,函数原型如下。
int rawsock = socket(AF_INET, SOCK_RAW, protocol);
在原始套接字中,第三个参数不像标准套接字那样设置为0,而是根据具体的协议设置不同的协议类型。我们变成中主要用到的协议类型如下所示:
IPPROTO_IP: ip协议,接收和发送的数据是IP包,包括IP的头;
IPPROTO_ICMP: ICMP协议,接收和发送的数据是ICMP数据包,可以根据设置来决定IP数据包头是否需要处理;
IPPROTO_UDP: UDP协议,接收和发送UDP数据包,IP数据包头可以根据设置来决定是否需要做处理;
IPPROTO_TCP: TCP协议,接收和发送TCP数据包,IP数据包头可根据设置决定是否需要处理;
IPPROTO_RAW: 原始IP包,不知道和IPPROTO_IP的具体区别是什么。
对于原始套接字的发送,没什么需要注意的,但是对于原始套接字的接收,有些需要注意的地方,如果不是对自定协议来说的话,首先,接收TCP和UDP数据不会传递给任何原始套接字接口,因为,在接收的这两个协议中都有设置port口,但是原始套接字没有绑定port口,所以不能接收这两种协议数据;其次,如果IP数据包是以分片的形式到达,那么内核协议会将所有到达的分片组合之后传给原始套接字。
2. 获取和设置套接字选项
对于网络套接字而言,有时候需要获取或者设置套接字的选项。获取和设置套接字选项的两个函数如下所示:
int getsockopt(int s,int level,int optname,void*optval,socklen_t *optlen);
int setsockopt(int s,int level,int optname,const void*optval,socklen_t optlen);
功能介绍:
通过上面两个函数可以获取和设置指定协议层级别的某一个套接字选项。函数执行成功时返回0,当失败时返回-1。
参数说明:
s:套接字文件描述符,通过socket创建;
level:套接字选项所在协议层级别;
optname:套接字选型名称,该参数与level是一一对应关系;
optval:套接字选项操作内存缓冲区。对于getsockopt来说,指向套接字选项返回值得缓冲区。对于setsockopt来说,指向设置参数的缓冲区。
optlen:optval参数的长度。对于getsockopt来说,是一个指向socklen_t类型的指针。对于setsockopt来说,是optval的实际长度。
套接字选项所在协议层的级别主要有SOL_SOCKET, IPPROTO_IP, IPPROTO_TCP等,每个级别的协议层都对应多个套接字选项。当套接字选项确定时,对应的协议层级别也就确定了。
简单的例子,如上面提到IP数据包的处理需要具体的设置来决定,其使用的是设置套接字选项操作,通过设置IP_HDRINCL选项可以决定IP头部是否需要用户自定义。套接字设置方法如下:
int set = 1;
setsockeopt(rawsock, IPPROTO_IP, IPHDRINCL, &set, sizeof(set));
功能介绍:
通过设置该套接字选项,编程者可以自定义IP头部结构。
参数说明:
rawsock:创建原始套接字返回的文件描述符;
IPPROTO_IP:创建原始套接字时选用的协议类型的级别,不同的套接字选项在不同的级别中设置,在此处IP_HDRINCL是在IPPROTO_IP的级别;
IP_HDRINCL:对应于设置是否自定义IP包头的套接字选项名;
set:设置套接字选项设置参数的缓冲区,函数最后一个参数是第4个参数的数据长度。
3. 主机字节序和网络字节序
因为处理器和操作系统不同,导致大于一个字节的数据在内存中的存放顺序不同,产生了字节序的概念。通常情况下,字节序分为大端字节序和小端字节序。大端字节序的定义是数据的低位字节存放在高地址,高位字节存放在低地址;小端字节序是数据的低位字节存放在低地址,高位字节存放在高地址。
由于主机的处理器的千差万别,所以对网络数据做了统一,网络字节序采用大端字节序进行传输,当主机字节序是小端字节序时,会将数据转换成大端字节序并进行传输,当主机是大端字节序时,无需转换直接传输。但是往往编程者不去关注到底主机是大端还是小端字节序,所以有专门的函数来实现这个功能,小端转大端,大端不变转换。同样的也有函数将网络大端字节序转换成主机小端字节序或者主机大端字节序。主要的函数如下所示(h代表主机,n代表网络,l代表长整型,s代表短整型):
uint32_t htonl(uint32_t hostlong); 将主机中的32位长整型字节序转换成网络大端的32位长整型字节序。
uint16_t htons(uint16_t hostshort); 将主机中的16位长整型字节序转换成网络大端的16位长整型字节序。
uint32_t ntohl(uint32_t netlong); 将网络大端32位长整型字节序转换成主机32位长整型字节序。
uint16_t ntons(uint16_t netshort); 将网络大端16位长整型字节序转换成主机16位长整型字节序。
4. 十进制点分字符串IP地址和二进制IP地址的转换
对于我们记忆来说往往是选择字符串IP地址来使用,但是真正的在网络中作为数据传输的IP地址则是二进制的。对于Linux来说有专门的函数来实现这两种地址之间的转换,函数原型如下:
int inet_aton(const char *string, struct in_addr *addr);
将string中存储的十进制字符串IP地址转换成二进制的IP地址,转换后的值保存在指针addr指向的struct in_addr地址中。该函数对255.255.255.255这个特殊IP地址返回有效IP地址,函数为不可重入函数。当成功执行,函数返回值非0,传入的地址非法时,返回值0.
in_addr_t inet_network(const char *cp);
两者都将十进制字符串IP形式转换为二进制IP形式,返回整型数。不同的是inet_addr返回网络字节序,inet_network返回主机字节序。两个函数对255.255.255.255这个特殊IP地址返回无效IP地址。
char *inet_ntoa(struct in_addr in);
输入网络字节序,如果正确,返回一个字符指针,该指针指向的内存区域是静态的,每次调用inet_ntoa时该区域都会被覆盖;错误,返回NULL。
#include <stdio.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { struct in_addr ip, locl, network; in_addr_t ret; char addr1[] = "192.158.1.1"; char addr2[] = "255.255.255.255"; int err; /*=============inet_aton============================*/ err = inet_aton(addr1, &ip); if(err) { printf("inet_aton: string IP %s value is 0x%x\n",addr1, ip.s_addr); } else { printf("inet_aton: string parameter %s is invalide!\n", addr1); } err = inet_aton(addr2, &ip); if(err) { printf("inet_aton: string IP %s value is 0x%x\n",addr2, ip.s_addr); } else { printf("inet_aton: string parameter %s is invalide!\n", addr2); } /*=============inet_aton============================*/ /*=============inet_addr============================*/ ip.s_addr = inet_addr(addr1); if(ip.s_addr != -1) { printf("inet_addr: string IP %s value is 0x%x\n",addr1, ip.s_addr); } else { printf("inet_addr: string parameter %s is invalide!\n", addr1); } ip.s_addr = inet_addr(addr2); if(ip.s_addr != -1) { printf("inet_addr: string IP %s value is 0x%x\n",addr2, ip.s_addr); } else { printf("inet_addr: string parameter %s is invalide!\n", addr2); } /*=============inet_addr============================*/ /*=============inet_network============================*/ ip.s_addr = inet_network(addr1); if(ip.s_addr != -1) { printf("inet_network: string IP %s value is 0x%x\n",addr1, ip.s_addr); } else { printf("inet_network: string parameter %s is invalide!\n", addr1); } ip.s_addr = inet_network(addr2); if(ip.s_addr != -1) { printf("inet_network: string IP %s value is 0x%x\n",addr2, ip.s_addr); } else { printf("inet_network: string parameter %s is invalide!\n", addr2); } /*=============inet_network============================*/ /*=============inet_addr/inet_network============================*/ char *str=NULL,*str1=NULL; ip.s_addr = inet_addr(addr1); str = inet_ntoa(ip); printf("inet_ntoa: ip %x string IP is %s\n", ip.s_addr, str); ip.s_addr = inet_addr(addr2); str1 = inet_ntoa(ip); printf("inet_ntoa: ip %x string IP is %s\n", ip.s_addr, str1); ip.s_addr = inet_network(addr1); str = inet_ntoa(ip); printf("inet_ntoa: ip %x string IP is %s\n", ip.s_addr, str); ip.s_addr = inet_network(addr2); str1 = inet_ntoa(ip); printf("inet_ntoa: ip %x string IP is %s\n", ip.s_addr, str1); ip.s_addr = inet_addr(addr1); str = inet_ntoa(ip); ip.s_addr = inet_addr(addr2); str1 = inet_ntoa(ip); printf("inet_ntoa: ip address2 %x string IP is %s, ip address1 previous string IP is %s\n", ip.s_addr, str1, str); /*=============inet_network============================*/ return 0; }
代码运行结果如下所示:
inet_pton()和inet_ntop()两个函数是可以兼容IPV4和IPV6的两个函数,可以实现字符串IP地址和二进制IP地址之间的转换。
int inet_pton(int af, const char *src, void *dst);
函数功能:
该函数是将字符串类型的IP地址转换成二进制类型,当函数返回-1时,是由于af协议族不支持造成的,当函数返回0时,表示字符串IP地址是不合法的。当返回正值时,表示转换成功。
参数说明:
af:网络类型的协议族,在IPv4下的值时AF_INET;
src:表示需要转换的字符串类型的IP地址;
dst:指向转换后的结果,不同的协议族,dst指向的结构体不同,如IPv4,dst指向结构struct in_addr指针。
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
函数功能:
该函数是将二进制IP地址转换成字符串IP地址,返回的字符串地址放在dst指针中,当发生错误时,返回NULL,当af协议族不支持时返回errno为EAFNOSUPPORT;当dst缓冲区过小的时候返回errno为ENOSPC。
参数说明:
af:表示网络协议族;
src:需要转换的二进制IP地址,在IPv4下,src指向一个struct in_addr结构类型的指针;
dst:指向字符串IP地址的缓冲区地址指针;
cnt:dst缓冲区的大小。
inet_ntop和inet_pton的函数实例如下所示:
#include <stdio.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> int main(void) { struct in_addr ip; char str[] = "192.168.1.1"; const char *str1 = NULL; char addr[16]; int err; /*=============inet_pton====================*/ err = inet_pton(AF_INET, str, &ip); if(err) { printf("inet_pton: ip %s value is 0x%x\n", str, ip.s_addr); } /*=============inet_ntop====================*/ str1 = (const char *)inet_ntop(AF_INET, (void *)&ip, (char *)&addr[0], 16); if(err) { printf("inet_ntop: 0x%x ip is %s\n", ip.s_addr, addr); } return 0; }
运行结果如下所示:
运行结果很简单,就是将二进制IP地址和字符串IP地址之间的转换,主要注意的就是两个函数的使用是如何使用的,因为个人老是混淆这两个函数,所以在此做一个标记。
5. 处理数据链路层的两种方式
在应用层可以通过SOCK_PACKET类型的协议族实现部分数据链路层的访问,在创建socket套接字的时候选择SOCK_PACKET类型,内核将不会对网络数据进行处理而是直接将数据交给用户,数据将从网卡的协议栈直接交给用户,创建函数如下所示:
socket(AF_INET, SOCK_PACKET, htons(0x0003));
参数说明:
AF_INET:表示IPv4网络协议族;
SOCK_PACKET:表示截取的数据实在物理层,数据不做处理将从网络协议栈直接交给用户处理。
0x0003:表示截取的数据帧的类型不确定,将会处理所有的包。
其实数据链路层的数据访问就是获取了最底层的数据帧,如果想要对数据做处理,需要一层层的剥离协议包头,根据包头处理响应的数据。如果想要实现监听处于同一个局域网的其他主机的网络数据,需要将网卡设置成混杂模式,并且要与被监听的主机处于同一个HUB的局域网,否则只能接受其他主机的广播包。
u8 *ethname = "eth0"; struct ifreq ifr; sockfd = socket(PF_PACKET,SOCK_RAW, htons(ETH_P_ALL)); if(sockfd < 0) { perror("Create socket failed!\n"); exit(-1); } strcpy(ifr.ifr_name, ethname); i = ioctl(sockfd, SIOCGIFFLAGS,&ifr); if(i < 0) { close(sockfd); printf("can‘t get flags \n"); exit (-1); } ifr.ifr_flags |= IFF_PROMISC; i = ioctl(sockfd, SIOCSIFFLAGS, &ifr); if(i < 0) { close(sockfd); printf("can‘t set flags \n"); exit (-1); }
设置网卡的混杂模式,使用了ioctl的SIOCGIFFLAGS和SIOCSIFFLAGS命令,对于这两个命令的使用需要注意的是,首先获取标志位,然后通过或的方式加上标志位,最后写入标志位,这样操作的目的是防止因为改动某一个标志位,而将其他的改掉。
另一种获取数据链路层的方法是:
struct sockaddr_ll sockaddr, dest_sock; sockfd = socket(PF_PACKET,SOCK_RAW, htons(ETH_P_ALL)); //socket套接字创建 bzero(&sockaddr, sizeof(struct sockaddr_ll)); sockaddr.sll_family = PF_PACKET; sockaddr.sll_ifindex = if_nametoindex("eth0"); //sockaddr.sll_ifindex = ifr.ifr_ifindex; sockaddr.sll_protocol = htons(ETH_P_ALL); //local socket地址配置 len = sendto(sockfd, ptr, ETH_MIN_DATA,0, (struct sockaddr *)&sockaddr,sizeof(struct sockaddr_ll)); //数据发送
对于使用此方法实现数据链路层数据的处理,在设置socket地址的配置参数,通过此种方法设置"sll_ifindex = if_nametoindex("eth0")"有效;通过设置sll_ifindex = ifr.ifr_ifindex的方法却不能实现数据正常发送,不知道是不是这种设置方式哪里还不对导致,如果使用此方法来处理数据链路层数据时需注意。
以上是关于Linux Socket 原始套接字编程的主要内容,如果未能解决你的问题,请参考以下文章