socket协议族
socket常用的协议有PF_INET
和PF_PACKET
,是AF_INET
协议族下的两类,具体可以参考socket协议族。
PF_INET
表示Internet上使用的是TCP/IP协议,提供SOCK_STREAM、SOCK_DGRAM、SOCK_RAW
三种服务,是常用的套接字协议。PF_PACKET
类型的socket是一类允许应用程序不经过网络协议栈而直接读写网络设备的接口,应用程序必须自己提供包括第 2 层报头在内的各级报文头部。这种类型常用以实现嗅探器 ,为了获得各种协议类型的报文 ,常配合ETH_P_ALL 类型来匹配第 2 层收到的各类报文。PF_PACKET 支持SOCK_DGRAM、SOCK_RAW
两种服务类型。前者由内核添加或者删除Ethernet 帧头部,后者可以由应用程序对 Ethernet 帧头部填充。PF_PACKET
常用作构造原始套接字。
在Linux系统中由 libpcap 库实现了对PF_PACKET 套接字的封装,提供一组更高级、更简单易用的接口。
INET协议族的原始套接字和PACKET协议族区别体现在创建和接收报头的能力不同:
- INET 协议族的原始套接字可以创建第 4 层及其以上各层协议的报头,配合
IP_HDRINCL
选项也可以创建IP 报头,但是网络层必须使用 IP 协议 - PF_PACKET 协议族要求用户提供第 2 层及其以上的各级报头,因此适用于其他各种网络应协议。
- 在接收时,INET 协议族的原始套接字只能获得 IP 报文的负载部分,而无法获得 IP 报头; PF_PACKET 协议族不但可以获得 IP 报头,还可以获得 Ethernet 帧头部的内容。
socket 地址结构体
socket地址结构体用于创建套接字时标识一个套接字
struct in_addr
{
in_addr_t s_addr;
}; //表示ip地址的结构体
struct sockaddr{
unsigned short sa_farnily;
char sa_data[14];
}; //socket通用的地址结构体,一般只在创建套接字时使用
struct sockaddr_in{
unsigned short sin_family; //一般为AF_INET
unsigned short int sin_port; //需要监听的端口号
struct in_addr sin_addr; //设置为INADDR_ANY,表示可以和任何主机通信
unsigned char sin_zero[8] //是用来填充的
}; //AF_INET专用的地址结构体
详细情况可以参考 socket地址结构体
TCP报文
TCP 连接的两端使用两对IP地址和端口识别这个连接,并且向监听这个端口的应用程序发送数据 。TCP 报头的固定长度是 20 个字节, 如果使用一些选项,最大长度为 60 个字节。TCP 报头格式为:
各字段依次为:
- 源端口:16 位,本地 TCP 端口号。
- 目的地端口:16 位,目的 TCP 端口号。
- 序号:32 位,用来跟踪发送报文字节顺序的序号。
- 确认编号:32 位,表示已经正确接收字节的序号 。
- 报头长度:4 位,表示以 4 个字节为单位的报头长度 。 如果不使用选项,报头长度值为 5。
- 保留:6 位,留做以后使用。
- 标记flags:6 位,每一位标记都有具体的含义。
- URG:紧急字段指针。如果为1,此时 TCP 协议报头结构中的紧急指针有效,表示数据包中包含紧急数据 。
- ACK:确认标志位。如果为 1 ,表示包中的确认号有效 。
- PSH:推送功能 。 如果为 1 ,表示接收端应尽快将数据传送给应用层 。
- RST:重置位,用来复位一个连接 。 RST 标志置位为1的数据包称为复位包 。 一般如果 TCP 收到的一个分段明显不属于该主机上的任何一个连接,则向远端发送一个复位包。
- SYN:用来建立连接,让连接双方同步的序列号。如果 SYN=1且 ACK =0 ,表示 数据包为连接请求,如果 SYN=1且ACK=1,则表示接受连接。
- FIN:表示发送端已经没有数据要求传输了,请求释放连接。
- 窗口尺寸:16 位,表示要求对方必须维持的窗口字节数。
- 校验和:16 位, TCP 报头和数据的校验和 。
- 紧急指 针:16 位,指向跟在 URG 数据后面的数据的序列号的偏移值
- 选项:MSS 、窗口比例等。
tcphdr
#include <netinet/tcp.h> // tcp头部
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
__be32 ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__be16 window;
__be16 check;
__be16 urg_ptr;
};
UDP报文
#include <netinet/udp.h> // udp头部
/* UDP header as specified by RFC 768, August 1980. */
struct udphdr
{
__extension__ union
{
struct
{
u_int16_t uh_sport; /* source port */
u_int16_t uh_dport; /* destination port */
u_int16_t uh_ulen; /* udp length */
u_int16_t uh_sum; /* udp checksum */
};
struct
{
u_int16_t source;
u_int16_t dest;
u_int16_t len;
u_int16_t check;
};
};
};
IP数据报
#include <netinet/ip.h> // ip头部
struct iphdr
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ihl:4;
unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
unsigned int version:4;
unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
u_int8_t tos; //服务类型
u_int16_t tot_len; //总长度
u_int16_t id; //标识
u_int16_t frag_off; //标志+片偏移
u_int8_t ttl; //生存时间
u_int8_t protocol; //协议
u_int16_t check; //头校验和
u_int32_t saddr; //源IP地址
u_int32_t daddr; //目的IP地址
/*The options start here. */
};
IPv6报文
IPv6的实现代码在头文件include/net/ipv6.h
中,具体的几个函数有:
- 在数据链路层发现网络层使用IPv6 协议时调用函数
ipv6_ rcv
来接收并继续处理该报文 - 当上层协议(如TCP 或 UDP)发送IPv6报文时,调用
ip6_ xmit
发送函数 - 通过IPv6发送 ICMP 报文时调用
icmpv6_send
函数 - IPv6转发报文的工作由
ip6_forward
函数完成
另外在net/ipv6/ip6_fib. c
中实现了针对 IPv6地址的路向表管理,在net/ipv6/tcp_ipv6. c
中实现了在IPv6 条件下 TCP协议必须进行的修改 。
IGMP报文
IGMP协议用以管理组播通信的成员,在头文件net/ipv4/igmp.c
中实现。IGMP报文封装在IP报文中,由IP报文头部的服务类型指定,调用igmp_rcv
函数处理。
ARP报文
ARP 协议的实现代码保存在include/net/if_arp.h
和net/core/ipv4/arp.c
中,由arp_send
和 arp_rcv
函数发送和接收ARP报文。
硬件类型,1代表是以太网。
协议类型,表明上层协议的类型,这里是0x0800,表示上层协议是IP协议
硬件地址长度,毫无疑问是6个字节
协议长度,这里IP协议的长度是4个字节
操作类型,在报文中占2个字节,1表示ARP请求,2表示ARP应答,3表示RARP请求,4表示RARP应答
ICMP报文
#include <netinet/ip_icmp.h> //icmp头
struct icmphdr
{
};
struct icmp
{
u_int8_t icmp_type; /* type of message, see below */
u_int8_t icmp_code; /* type sub code */
u_int16_t icmp_cksum; /* ones complement checksum of struct */
union
{
u_char ih_pptr; /* ICMP_PARAMPROB */
struct in_addr ih_gwaddr; /* gateway address */
struct ih_idseq /* echo datagram */
{
u_int16_t icd_id;
u_int16_t icd_seq;
} ih_idseq;
u_int32_t ih_void;
/* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
struct ih_pmtu
{
u_int16_t ipm_void;
u_int16_t ipm_nextmtu;
} ih_pmtu;
struct ih_rtradv
{
u_int8_t irt_num_addrs;
u_int8_t irt_wpa;
u_int16_t irt_lifetime;
} ih_rtradv;
} icmp_hun;
常用函数
对套接字操作的常用函数有以下几个,都包含在以下头文件中:
#include <sys/types.h>
#include <sys/socket.h>
发送函数
- write :调用 connect 函数建立连接之后,可 以使用 write 函数来发送数据。但 write 函数使用的是文件的写功能,无法支持 socket 通信时所特有的功能,如发送带外数据等。 - 建立连接后,将buf 中 nbytes 字节的内容写入文件描述符:
ssize_t write (int sockfd, const void * buf, size_t nbytes);
- sendmsg:可以一 次发送多个数据缓冲区的内容,但要求用户自行创建所需的
msghdr
结构。msghdr 是一种可以实现批量数据传递的数据结构。
ssize_t sendmsg(int sockfd, const struct msghdr* msg, int flags);
- send:send 函数与
write
函数十分相似,在保留 write 函数的三个参数 的同时,增加 了一个flags 参数用来实现一些socket所特有的功能,例如设定MSG__OOB
标志来发送带外数据 。
ssize_t send(int sockfd, const void*buf, size_t len, int flags) ;` //flags一般为0,同write
- sendto:
sendto
函数来发送非连接的 socket 数据,提供了设定报文目的地址和目的端口的能力。当用于面向连接的通
信时(套接字类型为SOCK_STREAM
),将省略参数to
和tolen
。
ssize_t sendto(int sockfd, const void*buf, size_t len, int flags, const struct sockaddr* to, socklen_t tolen)
一般情况下, buf 指针所指向的数据包不包含IP头,只包含IP报文的负载部分。如果要处理IP报头,则需在之前调用setsocketopt 函数设置套接字的选项,此时buf将包含IP头部。代码如下:
int flag= 1;
setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &flag, sizeof(int));
接收函数
- read 从文件描述符读取内容到buf:
ssize_t read(int sockfd, void* buf , size_t nbyte);
- recvmsg
ssize_t recvmsg(int sockfd, struct msghdr* msg , int flags);
- recv
ssize t recv(int sockfd, const void*buf, size_t len, int flags);
- recvfrom 性质同sendto
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* from, socklen_t* fromlen);
//flags用于控制是否接收带外数据
一般在接收数据时,recvfrom 函数处于阻塞状态,直到收到一个数据包才会返回 。如果希望 recvfrom 在接收数据包时处于非阻塞状态,则需要调用函数 fentl
将套接字设置为非阻塞筷式。具体代码如下:
fcntl(int sockfd, F_SETFL, O_NONBLOCK);
其他函数
- 创建套接字:
int socket (int domain, int type, int protocol);` // protocol:IPPROTO_IP 、IPPROTO_TCP 、IPPROTO_UDP 、 IPPROTO_ICMP
- 绑定端口:
int bind (int sockfd, const struct sockaddr* my_addr, socklen_t addrl en) ;
- 监听请求:
int listen (int sockfd, int backlog);` //backlog:请求队列的最大长度。
- 服务器端监听到请求后建立连接:
int accept (int sockfd, struct sockaddr* addr, socklen_t *addrlen);`//addr: 客户端结构体sockaddr的指针 。
- 客户端向服务器端发起连接请求:
int connect(int sockfd, const struct sockaddr * serv_addr , socklen_t addrlen);`//服务部端的连接信息
- 设置当前套接字的选项:
int setsockopt(int sockfd, //
int level, //表示控制套接字的层次,SOL_SOCKET(通用套接字选项),IPPROTO_IP和IPPROTO_TCP
int optname, //需要设置选项的名称,如果需要构建数据包的IP头就将Optname设为IP_HDRINCL
const char vo1d* optval, //指向存放选项值的缓冲区。optval需要根据选项不同的数据类型进行转换;
socklen_t * optlen //表示optval所指向的缓冲区的长度。);
linux下socket编写常用头文件:
<sys/socket.h> //与套接字相关的函数声明和结构体定义,如socket()、bind()、connect()及struct sockaddr的定义等
<sys/types.h> //包含很多类型重定义,如pid_t、int8_t等
<arpa/inet.h> //inet_addr,inet_ntop()、inet_ntoa()等
<netdb.h> //某些结构体定义、宏定义和函数声明,如struct hostent、struct servent、gethostbyname()、gethostbyaddr()、herror()等. 文件如其名,包括结构hostent(主机环境),获得主机的信息的几个函数(gethostbyname)。似乎这个就是定义主机的各项环境,例如hostname等等
<net/ethernet.h> // 包括几个以太网的数据结构,ether_addr(mac帧结构),ether_header(以太帧的头部)
<netinet/in.h> // sockaddr_in, "man 7 ip" ,htons
<netinet/in.h> //某些结构体声明、宏定义,如sockaddr_in、PROTO_ICMP、INADDR_ANY, 网络字节转换(ntoh,hton)
<netinet/ip.h> // 这个头文件和linux/ip.h似乎很相似,也有iphdr的数据结构,同时还包括了bsd中的ipheader结构定义。
<netinet/if_ether.h> //ether_arp的数据结构
<netinet/ether.h> // 以太祯的网络字节和ascii字节的转换,包括ether_ntoa(),ether_aton这样的函数定义
<linux/ip.h> //iphdr的数据结构,以及一些ip层的数据定义,同理的还有tcp.h,udp.h等等
<linux/if.h> //主要的socket头文件,似乎修改自unix的if.h,定义了网卡的接口信息的宏,例如IFF_UP.另外有数个重要的interface的数据结构定义,包括ifreq,ifconf,ifmap
<linux/if_packet.h> // 原始数据包的数据结构定义,包括sockaddr_pkt,sockaddr_ll,想接收原始数据包的不能错过这个文件。同理的还有if_ppp.h,if_tun.h等等
<unistd.h> //read,write
<poll.h> //poll,pollfd
<error.h> //perror
<stdio.h> //C语言标准输入输出库
<errno.h> //errno
<stdlib.h> //标准库
<string.h> // memset
<string>
<iostream>
<stdlib.h> //标准库
<sys/ioctl.h> //I/O控制操作相关的函数声明,如ioctl()
具体参见linux下socket编程常用头文件
下面为部分内容:
sys/types.h:数据类型定义
sys/socket.h:提供socket函数及数据结构
netinet/in.h:定义数据结构sockaddr_in
arpa/inet.h:提供IP地址转换函数
netdb.h:提供设置及获取域名的函数
sys/ioctl.h:提供对I/O控制的函数
sys/poll.h:提供socket等待测试机制的函数
其他在网络程序中常见的头文件
unistd.h:提供通用的文件、目录、程序及进程操作的函数
errno.h:提供错误号errno的定义,用于错误处理
fcntl.h:提供对文件控制的函数
time.h:提供有关时间的函数
crypt.h:提供使用DES加密算法的加密函数
pwd.h:提供对/etc/passwd文件访问的函数
shadow.h:提供对/etc/shadow文件访问的函数
pthread.h:提供多线程操作的函数
signal.h:提供对信号操作的函数
sys/wait.h、sys/ipc.h、sys/shm.h:提供进程等待、进程间通讯(IPC)及共享内存的函数
建议: 在编写网络程序时,可以直接使用下面这段头文件代码
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <malloc.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <stdarg.h>
#include <fcntl.h>
#include <fcntl.h>
涉及到用户权限及密码验证问题时加入如下语句:
#include <shadow.h>
#include <crypt.h>
#include <pwd.h>
需要注意的是,应该在编译时链接加密算法库,即增加编译选项:
-lcrypt
涉及到文件及时间操作加入如下语句:
#include <sys/time.h>
#include <utime.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/file.h>
涉及到多进程操作时加入如下语句:
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
涉及到多线程操作时加入如下语句:
#include <pthread.h>
#include <sys/poll.h>
需要注意的是,应该在编译时链接线程库,即增加编译选项:-lthread