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 != -)
{
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 != -)
{
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 != -)
{
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 != -)
{
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 ;
}
代码运行结果如下所示:
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[];
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[], );
if(err)
{
printf("inet_ntop: 0x%x ip is %s\n", ip.s_addr, addr);
} return ;
}
运行结果如下所示:
运行结果很简单,就是将二进制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 < )
{
perror("Create socket failed!\n");
exit(-);
} strcpy(ifr.ifr_name, ethname);
i = ioctl(sockfd, SIOCGIFFLAGS,&ifr);
if(i < )
{
close(sockfd);
printf("can't get flags \n");
exit (-);
}
ifr.ifr_flags |= IFF_PROMISC;
i = ioctl(sockfd, SIOCSIFFLAGS, &ifr);
if(i < )
{
close(sockfd);
printf("can't set flags \n");
exit (-);
}
设置网卡的混杂模式,使用了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,, (struct sockaddr *)&sockaddr,sizeof(struct sockaddr_ll)); //数据发送
对于使用此方法实现数据链路层数据的处理,在设置socket地址的配置参数,通过此种方法设置"sll_ifindex = if_nametoindex("eth0")"有效;通过设置sll_ifindex = ifr.ifr_ifindex的方法却不能实现数据正常发送,不知道是不是这种设置方式哪里还不对导致,如果使用此方法来处理数据链路层数据时需注意。
Linux Socket 原始套接字编程的更多相关文章
- Linux网络编程——原始套接字编程
原始套接字编程和之前的 UDP 编程差不多,无非就是创建一个套接字后,通过这个套接字接收数据或者发送数据.区别在于,原始套接字可以自行组装数据包(伪装本地 IP,本地 MAC),可以接收本机网卡上所有 ...
- 关于linux 原始套接字编程
关于linux 网络编程最权威的书是<<unix网络编程>>,但是看这本书时有些内容你可能理解的不是很深刻,或者说只知其然而不知其所以然,那么如果你想搞懂的话那么我建议你可以看 ...
- Linux系统C语言socket tcp套接字编程
1.套接字的地址结构: typedef uint32_t in_addr_t; //32位无符号整数,用于表示网络地址 struct in_addr{ in_addr_t s_addr; //32位 ...
- Python原始套接字编程
在实验中需要自己构造单独的HTTP数据报文,而使用SOCK_STREAM进行发送数据包,需要进行完整的TCP交互. 因此想使用原始套接字进行编程,直接构造数据包,并在IP层进行发送,即采用SOCK_R ...
- Python原始套接字编程-乾颐堂
在实验中需要自己构造单独的HTTP数据报文,而使用SOCK_STREAM进行发送数据包,需要进行完整的TCP交互. 因此想使用原始套接字进行编程,直接构造数据包,并在IP层进行发送,即采用SOCK_R ...
- 005.TCP--拼接TCP头部IP头部,实现TCP三次握手的第一步(Linux,原始套接字)
一.目的: 自己拼接IP头,TCP头,计算效验和,将生成的报文用原始套接字发送出去. 若使用tcpdump能监听有对方服务器的包回应,则证明TCP报文是正确的! 二.数据结构: TCP首部结构图: s ...
- Linux网络编程——原始套接字实例:MAC 头部报文分析
通过<Linux网络编程——原始套接字编程>得知,我们可以通过原始套接字以及 recvfrom( ) 可以获取链路层的数据包,那我们接收的链路层数据包到底长什么样的呢? 链路层封包格式 M ...
- Linux网络编程:原始套接字简介
Linux网络编程:原始套接字编程 一.原始套接字用途 通常情况下程序员接所接触到的套接字(Socket)为两类: 流式套接字(SOCK_STREAM):一种面向连接的Socket,针对于面向连接的T ...
- UNIX网络编程——原始套接字的魔力【上】
基于原始套接字编程 在开发面向连接的TCP和面向无连接的UDP程序时,我们所关心的核心问题在于数据收发层面,数据的传输特性由TCP或UDP来保证: 也就是说,对于TCP或UDP的程序开发,焦点在Dat ...
随机推荐
- 深入浅出Redis-redis底层数据结构(上)
1.概述 相信使用过Redis 的各位同学都很清楚,Redis 是一个基于键值对(key-value)的分布式存储系统,与Memcached类似,却优于Memcached的一个高性能的key-valu ...
- 数塔问题(DP算法)自底向上计算最大值
Input 输入数据首先包括一个整数C,表示测试实例的个数,每个测试实例的第一行是一个整数N(1 <= N <= 100),表示数塔的高度,接下来用N行数字表示数塔,其中第i行有个i个整数 ...
- Winserver2012下mysql 5.7解压版(zip)配置安装
一.安装 下载mysqlzip版本mysql不需要运行可执行文件,解压即可,下载zip版本mysqlmsi版本mysql双击文件即可安装,相对简单,本文不介绍此版本安装 配置环境变量打开环境变量配置页 ...
- LeetCode - Two Sum
Two Sum 題目連結 官網題目說明: 解法: 從給定的一組值內找出第一組兩數相加剛好等於給定的目標值,暴力解很簡單(只會這樣= =),兩個迴圈,只要找到相加的值就跳出. /// <summa ...
- ASP.NET跨平台最佳实践
前言 八年的坚持敌不过领导的固执,最终还是不得不阔别已经成为我第二语言的C#,转战Java阵营.有过短暂的失落和迷茫,但技术转型真的没有想象中那么难.回头审视,其实单从语言本身来看,C#确实比Java ...
- 浅析Go语言的Interface机制
前几日一朋友在学GO,问了我一些interface机制的问题.试着解释发现自己也不是太清楚,所以今天下午特意查了资料和阅读GO的源码(基于go1.4),整理出了此文.如果有错误的地方还望指正. GO语 ...
- 如果你发现mysql的外键约束不管用了
不知为何我机子上的mysql竟然默认关闭外键约束,导致我试了好多遍都可以插入非法值,以下语句可以开启约束 SET foreign_key_checks = 1; (0则关闭) 备忘
- Jvm --- 常用工具
jps:虚拟机进程状况工具 JVM Process Status Tool. 可以列出所有目前正在运行虚拟机的进程. jps -l 详细参数: -q 输出LVMID,省略主类名称 -m 输出虚拟机进程 ...
- ABP源码分析二十六:核心框架中的一些其他功能
本文是ABP核心项目源码分析的最后一篇,介绍一些前面遗漏的功能 AbpSession AbpSession: 目前这个和CLR的Session没有什么直接的联系.当然可以自定义的去实现IAbpSess ...
- 设置Fn键 笔记本直接按F1-F12 无须按Fn键 Fn+F12改F12(联想小新300为例)
最近公司给配的笔记本联想小新300 80RT i7-6500U 4G内存 500G机械,后加装120G固态+4G内存 这样就感觉还不错了. 在使用这本子的时候,去了Win10,强行装了Win7.无线 ...