对于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. webp图片实践之路

    最近,我们在项目中实践了webp图片,并且抽离出了工具模块,整合到了项目的基础模板中.传闻IOS10也将要支持webp,那么使用webp带来的性能提升将更加明显.估计在不久的将来,webp会成为标配. ...

  2. Fis3的前端工程化之路[三大特性篇之内容嵌入]

    Fis3版本:v3.4.22 Fis3的三大特性 资源定位:获取任何开发中所使用资源的线上路径 内容嵌入:把一个文件的内容(文本)或者base64编码(图片)嵌入到另一个文件中 依赖声明:在一个文本文 ...

  3. 【探索】机器指令翻译成 JavaScript

    前言 前些时候研究脚本混淆时,打算先学一些「程序流程」相关的概念.为了不因太枯燥而放弃,决定想一个有趣的案例,可以边探索边学. 于是想了一个话题:尝试将机器指令 1:1 翻译 成 JavaScript ...

  4. 6.DNS公司PC访问外网的设置 + 主DNS服务器和辅助DNS服务器的配置

    网站部署之~Windows Server | 本地部署 http://www.cnblogs.com/dunitian/p/4822808.html#iis DNS服务器部署不清楚的可以看上一篇:ht ...

  5. EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解

    前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...

  6. ShenNiu.MVC管理系统

    本篇将要和大家分享的是一个简单的后台管理系统,这里先发个地址http://www.lovexins.com:8081/(登陆账号:youke,密码:123123:高级用户账号:gaoji,密码:123 ...

  7. 原生js+css3实现图片自动切换,图片轮播

    运用CSS3transition及opacity属性 制作图片轮播动画 自己这两天根据用js来控制触发CSS3中transition属性,从而写出来的以CSS3动画为基础,js控制过程的图片轮播 运用 ...

  8. peer not authenticated的终极解决方案

    一.前述 使用httpclient发起https请求时,可能会遇到如下异常: javax.net.ssl.SSLPeerUnverifiedException: peer not authentica ...

  9. Jvm 内存浅析 及 GC个人学习总结

    从诞生至今,20多年过去,Java至今仍是使用最为广泛的语言.这仰赖于Java提供的各种技术和特性,让开发人员能优雅的编写高效的程序.今天我们就来说说Java的一项基本但非常重要的技术内存管理 了解C ...

  10. C# 用SoapUI调试WCF服务接口(WCF中包含用户名密码的验证)

    问题描述: 一般调试wcf程序可以直接建一个单元测试,直接调接口. 但是,这次,我还要测试在接口内的代码中看接收到的用户名密码是否正确,所以,单一的直接调用接口方法行不通, 然后就想办法通过soapU ...