对于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_addr(const char *cp);
      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 ;
}

代码运行结果如下所示:

  运行结果分析:
  第1,3,5行:inet_aton,inet_addr, inet_network函数将字符串IP地址转换成二进制IP地址,inet_aton,inet_addr返回结果为网络字节序,inet_network返回结果为主机字节序;
  第2行:inet_aton认为255.255.255.255是正确的字符串IP地址;
  第4,6行:inet_addr, inet_network认为255.255.255.255是不正确的字符串IP地址;
  第7-10行:inet_ntoa将二进制IP地址转换成字符串IP地址,通过inet_addr和inet_network生成二进制IP地址,可以看出其转换成的字节序是网络字节序还是主机字节序;
  第11行:可以验证inet_ntoa是不可重入函数。
 

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 原始套接字编程的更多相关文章

  1. Linux网络编程——原始套接字编程

    原始套接字编程和之前的 UDP 编程差不多,无非就是创建一个套接字后,通过这个套接字接收数据或者发送数据.区别在于,原始套接字可以自行组装数据包(伪装本地 IP,本地 MAC),可以接收本机网卡上所有 ...

  2. 关于linux 原始套接字编程

    关于linux 网络编程最权威的书是<<unix网络编程>>,但是看这本书时有些内容你可能理解的不是很深刻,或者说只知其然而不知其所以然,那么如果你想搞懂的话那么我建议你可以看 ...

  3. Linux系统C语言socket tcp套接字编程

    1.套接字的地址结构: typedef uint32_t in_addr_t; //32位无符号整数,用于表示网络地址 struct in_addr{ in_addr_t s_addr; //32位 ...

  4. Python原始套接字编程

    在实验中需要自己构造单独的HTTP数据报文,而使用SOCK_STREAM进行发送数据包,需要进行完整的TCP交互. 因此想使用原始套接字进行编程,直接构造数据包,并在IP层进行发送,即采用SOCK_R ...

  5. Python原始套接字编程-乾颐堂

    在实验中需要自己构造单独的HTTP数据报文,而使用SOCK_STREAM进行发送数据包,需要进行完整的TCP交互. 因此想使用原始套接字进行编程,直接构造数据包,并在IP层进行发送,即采用SOCK_R ...

  6. 005.TCP--拼接TCP头部IP头部,实现TCP三次握手的第一步(Linux,原始套接字)

    一.目的: 自己拼接IP头,TCP头,计算效验和,将生成的报文用原始套接字发送出去. 若使用tcpdump能监听有对方服务器的包回应,则证明TCP报文是正确的! 二.数据结构: TCP首部结构图: s ...

  7. Linux网络编程——原始套接字实例:MAC 头部报文分析

    通过<Linux网络编程——原始套接字编程>得知,我们可以通过原始套接字以及 recvfrom( ) 可以获取链路层的数据包,那我们接收的链路层数据包到底长什么样的呢? 链路层封包格式 M ...

  8. Linux网络编程:原始套接字简介

    Linux网络编程:原始套接字编程 一.原始套接字用途 通常情况下程序员接所接触到的套接字(Socket)为两类: 流式套接字(SOCK_STREAM):一种面向连接的Socket,针对于面向连接的T ...

  9. UNIX网络编程——原始套接字的魔力【上】

    基于原始套接字编程 在开发面向连接的TCP和面向无连接的UDP程序时,我们所关心的核心问题在于数据收发层面,数据的传输特性由TCP或UDP来保证: 也就是说,对于TCP或UDP的程序开发,焦点在Dat ...

随机推荐

  1. 数学思想:为何我们把 x²读作x平方

    要弄清楚这个问题,我们得先认识一个人.古希腊大数学家 欧多克索斯,其在整个古代仅次于阿基米德,是一位天文学家.医生.几何学家.立法家和地理学家. 为何我们把 x²读作x平方呢? 古希腊时代,越来越多的 ...

  2. 如何优雅的使用RabbitMQ

    RabbitMQ无疑是目前最流行的消息队列之一,对各种语言环境的支持也很丰富,作为一个.NET developer有必要学习和了解这一工具.消息队列的使用场景大概有3种: 1.系统集成,分布式系统的设 ...

  3. 谈谈JS的观察者模式(自定义事件)

    呼呼...前不久参加了一个笔试,里面有一到JS编程题,当时看着题目就蒙圈...后来研究了一下,原来就是所谓的观察者模式.就记下来...^_^ 题目 [附加题] 请实现下面的自定义事件 Event 对象 ...

  4. 【知识必备】RxJava+Retrofit二次封装最佳结合体验,打造懒人封装框架~

    一.写在前面 相信各位看官对retrofit和rxjava已经耳熟能详了,最近一直在学习retrofit+rxjava的各种封装姿势,也结合自己的理解,一步一步的做起来. 骚年,如果你还没有掌握ret ...

  5. 代码的坏味道(17)——夸夸其谈未来性(Speculative Generality)

    坏味道--夸夸其谈未来性(Speculative Generality) 特征 存在未被使用的类.函数.字段或参数. 问题原因 有时,代码仅仅为了支持未来的特性而产生,然而却一直未实现.结果,代码变得 ...

  6. 来自于微信小程序的一封简讯

    9月21晚间,微信向部分公众号发出公众平台-微信应用号(小程序)的内测邀请,向来较为低调的微信在这一晚没人再忽视它了. 来自个人博客:Damonare的个人博客 一夜之间火了的微信应用号你真的知道吗? ...

  7. PHP设计模式(三)抽象工厂模式(Abstract Factory For PHP)

    一.什么是抽象工厂模式 抽象工厂模式的用意为:给客户端提供一个接口,可以创建多个产品族中的产品对象 ,而且使用抽象工厂模式还要满足以下条件: 系统中有多个产品族,而系统一次只可能消费其中一族产品. 同 ...

  8. 敏捷转型历程 - Sprint3 Grooming

    我: Tech Leader 团队:团队成员分布在两个城市,我所在的城市包括我有4个成员,另外一个城市包括SM有7个成员.另外由于我们的BA离职了,我暂代IT 的PO 职位.PM和我在一个城市,但他不 ...

  9. Mysql - 性能优化之子查询

    记得在做项目的时候, 听到过一句话, 尽量不要使用子查询, 那么这一篇就来看一下, 这句话是否是正确的. 那在这之前, 需要介绍一些概念性东西和mysql对语句的大致处理. 当Mysql Server ...

  10. click事件的累加绑定,绑定一次点击事件,执行多次

    最近做项目为一个添加按钮绑定点击事件,很简单的一个事情,于是我按照通常做法找到元素,使用jquery的on()方法为元素绑定了点击事件,点击同时发送请求.完成后看效果,第一次点击没有问题.再一次点击后 ...