Linux C Socket 编程
0 前言
主要描述在 Linux 下基于 C 语言的 Socket 编程,概览如下所示:
- 1 Socket 是什么
- 2 Socket 类型
- 标准套接字
- 流套接字(SOCK_STREAM)
- 数据报套接字(SOCK_DGRAM)
- 原始套接字(SOCK_RAW)
- 标准套接字
- 3 socket 函数介绍及其应用场景
- 4 TCP Socket 与 UDP Socket 通信过程
- 5 相关 API 函数
- bind 函数
- listen 函数
- accept 函数
- connect 函数
- sendto 函数
- recvfrom 函数
- 6 字节序介绍
- 7 代码示例
- 7.1 标准套接字
- 7.1.1 流套接字(SOCK_STREAM)
- 单个客户端单个服务器的 TCP 通信
- 多线程实现 - 单个客户端单个服务器的 TCP 通信
- 多路复用实现 - 单个客户端单个服务器的 TCP 通信
- 多个客户端单个服务器的 TCP 通信
- 多线程实现 - 多个客户端单个服务器的 TCP 通信
- 多路复用实现 - 多个客户端单个服务器的 TCP 通信
- 7.1.2 数据报套接字(SOCK_DGRAM)
- 单个客户端单个服务器的 UDP 通信
- 多线程实现 - 单个客户端单个服务器的 UDP 通信
- 多路复用实现 - 单个客户端单个服务器的 UDP 通信
- UDP 通信组播
- UDP 通信广播
- 7.2 原始套接字(SOCK_RAW)
- 抓取以太网上的所有数据帧
- 抓取以太网上的所有数据帧,匹配 HTTP 协议并发送 TCP RST
1 Socket 是什么
Socket(套接字),就是对 网络上进程通信 的 端点 的 抽象。一个 Socket 就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。
从所处的位置来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信交互的接口。如下图所示:
2 Socket 类型
2.1 标准套接字
标准套接字是在传输层使用的套接字,分为流式套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)。
标准套接字在接收和发送时只能操作数据部分(TCP Payload / UDP Payload),而不能对 IP 首部或TCP 首部和 UDP 首部进行操作。
2.1.1 流套接字(SOCK_STREAM)
流套接字(SOCK_STREAM)用于提供 面向连接(可靠)的数据传输服务。
流套接字保证数据能够实现无差错、无重复发数据,并按顺序接收。
流套接字(SOCK_STREAM)使用 TCP(The Transmission Control Protocol)协议 进行数据的传输。
2.1.2 数据报套接字(SOCK_DGRAM)
数据报套接字(SOCK_DGRAM)用于提供 无连接(不可靠)的数据传输服务。
数据报套接字不保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。
数据报套接字(SOCK_DGRAM)使用 UDP(User DatagramProtocol)协议 进行数据的传输。
2.2 原始套接字(SOCK_RAW)
原始套接字(SOCK_RAW)可以做到标准套接字做到的事,更可以做到标准套接字做不到的事。
原始套接字是在传输层及传输层以下使用的套接字。
原始套接字在接收和发送时不仅能操作数据部分(TCP Payload / UDP Payload),也能对 IP 首部或TCP 首部和 UDP 首部进行操作。
因此如果我们开发的是更底层的应用,比如发送一个自定义的 IP 包、UDP 包、TCP 包或 ICMP 包,捕获所有经过本机网卡的数据包(sniffer),伪装本机的 IP ,拒绝服务攻击(DOS)等,都可以通过原始套接字(SOCK_RAW)实现。
注意:必须在管理员权限下才能使用原始套接字。
3 Socket() 函数 介绍
3.1 功能
分配文件描述符,创建 socket,即创建网络上进程通信的端点。
3.2 头文件
#include <sys/types.h>
#include <sys/socket.h>
3.3 函数原型
int socket(int domain, int type, int protocol)
3.4 参数
注意:type 和 protocol 不可以随意组合,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 组合。
具体的组合和应用场景可以参考 4 创建 Socket 及其应用场景
3.4.1 domain
domain:即协议域,又称为协议族(family),如下所示:
-
AF_INET / PF_INET(2):IPv4,获取 网络层的数据
-
AF_INET6:IPv6
-
AF_UNIX:UNIX 系统本地通信
-
AF_PACKET / PF_PACKET(17):以太网包,获取 数据链路层的数据
注:
-
AF = Address Family(地址族),PF = Protocol Family(协议族)
-
理论上建立 socket 时是指定协议,应该用 PF_xxxx,设置地址时应该用 AF_xxxx。当然 AF_xxxx和 PF_xxxx 的值是相同的,混用也不会有太大的问题。
3.4.2 type
type:指定 socket 类型,如下所示:
-
SOCK_STREAM(1):面向连接的流式套接字(TCP)
-
SOCK_DGRAM(2):面向无连接的数据包套接字(UDP)
-
SOCK_RAW(3):接收 底层数据报文 的原始套接字
-
SOCK_PACKET(10):过时类型,可以使用,但是已经废弃,以后不保证还能支持,不推荐使用。
3.4.3 protocol
protocol:指定协议,如下所示:
-
0:自动选择 type 类型对应的默认协议。
-
IPPROTO_IP(0):接受 TCP 类型的数据帧
-
IPPROTO_ICMP(1):接受 ICMP 类型的数据帧
-
IPPROTO_IGMP(2)接受 IGMP 类型的数据帧
-
IPPROTO_TCP(6):接受 TCP 类型的数据帧
-
IPPROTO_UDP(17):接受 UDP 类型的数据帧
-
ETH_P_IP(0x800):接收发往本机 MAC 的 IP 类型的数据帧
-
ETH_P_ARP(0x806):接受发往本机 MAC 的 ARP 类型的数据帧
-
ETH_P_RARP(0x8035):接受发往本机 MAC 的 RARP 类型的数据帧
-
ETH_P_ALL(0x3):接收发往本机 MAC 的所有类型 IP ARP RARP 的数据帧,接收从本机发出的所有类型的数据帧。(混杂模式打开的情况下,会接收到非发往本地 MAC 的数据帧)
3.5 返回值
-
成功:返回一个文件描述符
-
失败:返回 -1,并设置 errno
3.6 备注
详情查看 man 手册:man 2 socket
3.7 创建 Socket 及其应用场景
4 TCP Socket 与 UDP Socket 通信过程
4.1 TCP Socket 通信过程
-
服务器过程
-
建立连接阶段
-
调用 socket(),分配文件描述符,创建 服务器 socket
-
调用 bind(),将 socket 与本地 IP 地址和端口绑定
-
调用 listen(),监听指定端口,socket() 创建的 socket 是主动的,调用 listen 使得该 socket 成为监听 socket ,变主动为被动
-
调用 accept(),获得 连接 socket,阻塞等待客户端发起连接
-
-
数据交互阶段
-
调用 read(),阻塞等待客户端发送的数据请求,收到请求后从 read() 返回,处理客户端请求
-
调用 write(),将数据发送给客户端
-
-
关闭连接
- 当 read() 返回 0 的时候,说明客户端发来了 FIN 数据包,即关闭连接,调用 close() 关闭 连接 socket 和 监听 socket
-
-
客户端过程
-
建立连接阶段
-
调用 socket(),分配文件描述符,创建 客户端 socket
-
调用 connect(),向服务器发送建立连接请求
-
-
数据交互阶段
-
调用 write(),向服务器发送数据
-
调用 read(),阻塞等待服务器应答
-
-
关闭连接
- 当没有数据发送的时候,调用 close() 关闭 客户端 socket ,即关闭连接,向服务器发送 FIN 数据报
-
4.2 UDP Socket 通信过程
-
服务器过程
-
建立连接阶段
-
调用 socket(),分配文件描述符,创建 服务器 socket
-
调用 bind(),将 socket 与本地 IP 地址和端口绑定
-
-
数据交互阶段
-
调用 recvfrom(),阻塞,接受客户端的数据
-
调用 sendto(),将数据发送给客户端
-
-
关闭连接
- 调用 close() 关闭 服务器 socket
-
-
客户端过程
-
建立连接阶段
- 调用 socket(),分配文件描述符,创建 客户端 socket
-
数据交互阶段
-
调用 sendto(),向服务器发送数据
-
调用 recvfrom(),阻塞,接受服务器的数据
-
-
关闭连接
- 调用 close() 关闭 客户端 socket ,即关闭连接。
-
5 相关 API 函数
5.1 bind() 函数
5.1.1 功能
将 IP 地址信息绑定到 socket。
5.1.2 头文件
#include <sys/types.h>
#include <sys/socket.h>
5.1.3 函数原型
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
5.1.4 参数
1、sockfd
通信 socket
2、 addr
要绑定的地址信息(包括IP地址,端口号)。
通用地址结构体定义:
struct sockaddr
{
sa_family_t sa_family; // 地址族, AF_xxx
char sa_data[14]; // 包括 IP 和端口号
}
新型的地址结构体定义:(查看新型的结构体信息: gedit /usr/include/linux/in.h )
struct sockaddr_in
{
__kernel_sa_family_t sin_family; // 地址族,IP 协议。默认:AF_INET
__be16 sin_port; // 端口号
struct in_addr sin_addr; // 网络 IP 地址
unsigned char __pad // 8 位的预留接口
};
3、 addrlen
地址信息大小
4、 返回值
-
成功:返回 0
-
失败:返回 -1,并设置 errno
5、 备注
详细查看 man 手册:man 2 bind
5.2 listen() 函数
5.2.1 功能
监听指定端口,socket() 创建的 socket 是主动的,调用 listen 使得该 socket 成为 监听 socket ,变主动为被动。
5.2.2 头文件
#include <sys/socket.h>
5.2.3 函数原型
int listen(int sockfd, int backlog);
5.2.4 参数
1、 sockfd
通信 socket
2、 backlog
同时能处理的最大连接要求
3、 返回值
-
成功:返回 0
-
失败:返回 -1,并设置 errno
4、 备注
详细查看 man 手册:man 2 listen
5.3 accept() 函数
5.3.1 功能
提取出 监听 socket 的等待连接队列中 第一个连接请求,创建 一个新的 socket,即 连接 socket
新建立的 连接 socket 用于发送数据和接受数据。
5.3.2 头文件
#include <sys/socket.h>
5.3.3 函数原型
#include <sys/types.h>
#include <sys/socket.h>
5.3.4 参数
1、 sockfd
监听 socket,即 在 调用 listen() 后的 监听 socket。
2、 addr
(可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。Addr参数的实际格式由套接口创建时所产生的地址族确定。
3、 addrlen
(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数。
4、 返回值
-
成功:指向 新的 socket(连接 socket)的文件描述符。
-
失败:返回 -1,并设置 errno
5、 备注
详细查看 man 手册:man 2 listen
5.4 connect() 函数
5.4.1 功能
发送连接请求
5.4.2 头文件
#include <sys/types.h>
#include <sys/socket.h>
5.4.3 函数原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
5.4.4 参数
1、 sockfd
通信 socket
2、 addr
要连接的服务器地址
3、 addrlen
地址信息大小
4、返回值
-
成功:返回 0
-
失败:返回 -1,并设置 errno
5、 备注
详细查看 man 手册:man 2 connect
5.5 sendto() 函数
5.5.1 功能
将数据由指定的 socket 传给对方主机
5.5.2 头文件
#include <sys/types.h>
#include <sys/socket.h>
5.5.3 函数原型
int sendto (int sockfd , const void * msg, int len, unsigned int flags, const
struct sockaddr * to , int tolen);
5.5.4 参数
1、 sockfd
已建立连接的 socket,如果利用 UDP 协议则不需建立连接。
2、 msg
发送数据的缓冲区。
3、 len
缓冲区长度。
4、 flags
调用方式标志位,一般设为 0 。
5、 to
用来指定要传送的网络地址,结构 sockaddr
6、 tolen
sockaddr 的长度
7、 返回值
-
成功:返回实际传送出去的字符数
-
失败:返回 -1,并设置 errno
8、 备注
详细查看 man 手册:man 2 sendto
5.6 recvfrom() 函数
5.6.1 功能
接收远程主机经指定的 socket 传来的数据,并把数据传到由参数 buf 指向的内存空间。
5.6.2 头文件
#include <sys/types.h>
#include <sys/socket.h>
5.6.3 函数原型
int recvfrom(int sockfd,void *buf,int len,unsigned int flags, struct sockaddr *from,int *fromlen);
5.6.4 参数
1、 sockfd
已建立连接的 socket,如果利用 UDP 协议则不需建立连接。
2、 buf
接收数据缓冲区。
3、 len
缓冲区长度。
4、 flags
调用方式标志位,一般设为 0 。
5、 from
(可选)指针,指向装有源地址的缓冲区,结构 sockaddr
6、 fromlen
(可选)指针,指向 from 缓冲区长度值,sockaddr 的结构长度
7、 返回值
-
成功:返回实际接受到的字符数
-
失败:返回 -1,并设置 errno
8、备注
详细查看 man 手册:man 2 recvfrom
6 字节序
字节序,是 大于一个字节类型的数据在内存中的存放顺序,由 CPU 架构决定,与操作系统无关。是在跨平台和网络编程中,时常要考虑的问题。
6.1 高低地址
在内存中,栈是向下生长的,以char arr[4]为例,(因为 char 类型数据只有一个字节,不存在字节序的问题)依次输出每个元素的地址,可以发现,arr[0] 的地址最低,arr[3] 的地址最高,如图:
6.2 高低字节
在十进制中靠左边的是高位,靠右边的是低位,在其他进制也是如此。
例如: 0x12345678,从高位到低位的字节依次是 0x12、0x34、0x56 和 0x78。
6.3 字节序分类 - 大小端模式
字节序被分为两类:
-
大端模式(Big-endian):内存的 低地址 存放 数据的高字节,内存的 高地址 存放 数据的低字节。(与人类阅读顺序一致)
-
** 小端模式**(Little-endian),是指内存的 低地址 存放 数据的低字节,内存的 高地址 存放 数据的高字节。
大端模式 CPU 代表是 IBM Power PC,小端模式 CPU 代表是 Intel X86、ARM。
6.4 大小端示例
以 0x12345678 为例,两种模式在内存中的存储情况,如下表所示:
6.5 判断大小端
利用 C 语言 union 联合体所有成员共用同一块内存的特性,可以用联合体快速实现判断大小端。
#include <stdio.h>
union u
{
char c[4];
int i;
};
int main(void)
{
union u test;
int j;
test.i = 0x12345678;
for(j = 0; j < sizeof(test.c); j++)
{
printf("0x%x\n",test.c[j]);
}
return 0;
}
运行后结果:
可以看出,我的机器是小端字节序。
6.6 网络字节序与本机字节序
网络字节序(NBO,Network Byte Order),是 TCP/IP 中规定好的一种数据表示格式。它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。
网络字节序采用大端(Big-endian)字节序排序方式。
主机字节顺序(HBO,Host Network Order),与机器 CPU 相关,数据的存储顺序由 CPU 决定。
6.6.1 转换函数
socket 编程中经常会用到 4 个网络字节顺序与本地字节顺序之间的转换函数:htons()、ntohl()、 ntohs()、htons()。
htonl()--"Host to Network Long" // 长整型数据主机字节顺序转网络字节顺序
ntohl()--"Network to Host Long" // 长整型数据网络字节顺序转主机字节顺序
htons()--"Host to Network Short" // 短整型数据主机字节顺序转网络字节顺序
ntohs()--"Network to Host Short" // 短整型数据网络字节顺序转主机字节顺序
在使用小端字节序的系统中,这些函数会把字节序进行转换。
在使用大端字节序的系统中,这些函数会定义成空宏。
7 代码示例
7.1 标准套接字(SOCK_STREAM - TCP)
7.1.1 单个客户端单个服务器的 TCP 通信
Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子1
7.1.2 多线程实现 - 单个客户端单个服务器的 TCP 通信
Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子2
7.1.3 多路复用实现 - 单个客户端单个服务器的 TCP 通信
Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子3
7.1.4 多个客户端单个服务器的 TCP 通信
Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子4
7.1.5 多线程实现 - 多个客户端单个服务器的 TCP 通信
Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子5
7.1.6 多路复用实现 - 多个客户端单个服务器的 TCP 通信
Linux-C TCP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子6
7.2 标准套接字(SOCK_DGRAM- UDP)
7.2.1 单个客户端单个服务器的 UDP 通信
代码来源:Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子1
7.2.2 多线程实现 - 单个客户端单个服务器的 UDP 通信
代码来源:Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子2
7.2.3 多路复用实现 - 单个客户端单个服务器的 UDP 通信
代码来源:Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子3
7.2.4 UDP 通信组播
代码来源:Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子4
7.2.5 UDP 通信广播
代码来源:Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子5
7.3 原始套接字
7.3.1 抓取以太网上的所有数据帧
代码来源:GitHub - zhouyingjiu - https://github.com/zouyingjiu/sniffer
/*
* sniffer.c
*
* 功能:
* linux rawSocket 抓取以太网上的所有数据帧
*
* 参数:
* 无
*
* 注意:
* 执行该程序需要 root 权限 sudo ./
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef __linux__
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netinet/ip_icmp.h>
#include <net/if_arp.h>
#include <netinet/if_ether.h>
#include <net/if.h>
#include <sys/ioctl.h>
#elif __win32__
#include <windows.h>
#endif
void UnpackARP(char *buff);
void UnpackIP(char *buff);
void UnpackTCP(char *buff);
void UnpackUDP(char *buff);
void UnpackICMP(char *buff);
void UnpackIGMP(char *buff);
int main(int argc, char **argv)
{
int sockfd, i;
char buff[2048];
/*
* 监听以太网上的所有数据帧
*/
if(0 > (sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))))
{
perror("socket error!");
exit(-1);
}
while(1)
{
memset(buff, 0, 2048);
int n = recvfrom(sockfd, buff, 2048, 0, NULL, NULL);
printf("%s\n",buff);
printf("开始解析数据包============\n");
printf("大小: %d\n", n);
struct ethhdr *eth = (struct ethhdr*)buff;
char *nextStack = buff + sizeof(struct ethhdr);
int protocol = ntohs(eth->h_proto);
switch(protocol)
{
case ETH_P_IP:
UnpackIP(nextStack);
break;
case ETH_P_ARP:
UnpackARP(nextStack);
break;
}
printf("解析结束=================\n\n");
}
return 0;
}
void getAddress(long saddr, char *str)
{
sprintf(str, "%d.%d.%d.%d", \
((unsigned char*)&saddr)[0], \
((unsigned char*)&saddr)[1], \
((unsigned char*)&saddr)[2], \
((unsigned char*)&saddr)[3]);
}
void UnpackARP(char *buff)
{
printf("ARP数据包\n");
}
void UnpackIP(char *buff)
{
struct iphdr *ip = (struct iphdr*)buff;
char *nextStack = buff + sizeof(struct iphdr);
int protocol = ip->protocol;
char data[20];
getAddress(ip->saddr, data);
printf("来源ip %s\n", data);
bzero(data, sizeof(data));
getAddress(ip->daddr, data);
printf("目标ip %s\n", data);
switch(protocol)
{
case 0x06:
UnpackTCP(nextStack);
break;
case 0x17:
UnpackUDP(nextStack);
break;
case 0x01:
UnpackICMP(nextStack);
break;
case 0x02:
UnpackIGMP(nextStack);
break;
default:
printf("unknown protocol\n");
break;
}
}
void UnpackTCP(char *buff)
{
struct tcphdr *tcp = (struct tcphdr*)buff;
printf("传输层协议:tcp\n");
printf("来源端口:%d\n", ntohs(tcp->source));
printf("目标端口:%d\n", ntohs(tcp->dest));
}
void UnpackUDP(char *buff)
{
struct udphdr *udp = (struct udphdr*)buff;
printf("传输层协议:udp\n");
printf("来源端口:%d\n", ntohs(udp->source));
printf("目的端口:%d\n", ntohs(udp->dest));
}
void UnpackICMP(char *buff)
{
printf("ICMP数据包\n");
}
void UnpackIGMP(char *buff)
{
printf("IGMP数据包\n");
}
7.3.2 抓取以太网上的所有数据帧,匹配 HTTP 协议并发送 TCP RST
代码来源:我的 Github - https://github.com/PikapBai/sniffer_cmpHTTP_sendTCP
8 参考资料
1、套接字 - 百度百科 - https://baike.baidu.com/item/套接字/9637606?fromtitle=socket
2、 RAW SOCKET - 百度百科 - https://baike.baidu.com/item/RAW SOCKET
3、原始套接字简介 - chengqiuming - https://blog.csdn.net/chengqiuming/article/details/89577351
4、原始套接字概述 - anton_99 - https://blog.csdn.net/anton_99/article/details/95646879
5、 Linux 原始套接字抓取底层报文 - 2603898260 - https://blog.csdn.net/s2603898260/article/details/85020006
6、Linux-C TCP 简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144
7、 【Linux网络编程】socket编程“网络字节顺序”和“主机字节顺序” - qq_20553613 - https://blog.csdn.net/qq_20553613/article/details/86385271
8、网络字节序 - 百度百科 - https://baike.baidu.com/item/网络字节序
9、 字节序(大小端)理解 - sunflower_della - https://blog.csdn.net/sunflower_della/article/details/90439935
10、 理解大小端字节序 - fan-yuan - https://www.cnblogs.com/fan-yuan/p/10406315.html
11、linux网络编程之TCP/IP的TCP socket通信过程(含实例代码) - 知乎 - linux服务器开发专栏 - https://zhuanlan.zhihu.com/p/148739946
12、 Linux C Socket UDP编程详解及实例分享 - 知乎 - linux服务器开发专栏 - https://zhuanlan.zhihu.com/p/131402832
13、 Linux-C UDP简单例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233
14、《图解 TCP/IP》(第 5 版)[日]竹下隆史 /[日]村山公保/ [日]荒井透 / [日]苅田幸雄
15、浅谈linux下原始套接字 SOCK_RAW 的内幕及其应用 - 知乎 - linux服务器开发专栏 - https://zhuanlan.zhihu.com/p/254912774
16、 GitHub - zhouyingjiu - https://github.com/zouyingjiu/sniffer