环境
- 操作系统: Linux
- 编译器: make
一、套接字
- TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字(socket)或插口。
- 套接字用(IP地址:端口号)表示。
- 它是网络通信过程中端点的抽象表示,包含进行网络通信必需的五种信息:
- 连接使用的协议
- 本地主机的IP地址
- 本地进程的协议端口
- 远地主机的IP地址
- 远地进程的协议端口。
1. 创建一个套接字
调用参考文档 socket() — Create a socket
1 |
|
1.1. 参数取值
__domain
指明使用的协议族
AF_INET
: Address Family,指定TCP/IP协议家族PF_INET
: Protocol Family- 在windows中
AF_INET
和PF_INET
完全一样 - 在某些Linux中两者会有差距(但一般也相同),理论上建立socket时是指定协议,应该用
PF_XXX
,设置地址时用AF_XXX
,不过在两者相等的情况下混用也没啥。
- 在windows中
AF_UNIX
: 域套接字,用于同一台计算机的进程间通信AF_INET6
:ipv6网络协议
__type
指明socket类型
SOCK_STREAM
: 流套接字,对应TCP协议SOCK_DGRAM
: 数据报套接字,对应UDP协议SOCK_RAW
: 原始套接字,提供原始网络协议存取SOCK_PACKET
: 直接从网络驱动获取数据,即从数据链路层开始处理(过时了)- 如果想获取数据链路层,可用
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))
- 如果想获取数据链路层,可用
__protocol
协议类型
- 传输层:
IPPROTO_TCP
、IPPROTO_UDP
、IPPROTO_ICMP
- 网络层:
htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
三者参数并不完全独立,比如type选用SOCK_STREAM
,protocol就得是IPPRPTP_TCP
2. 绑定协议地址
- 绑定地址后,可以使用地址进行读写和监听,这里会判断是否已被占用
- 如果端口想要复用,可以使用setsockopt设置socket为可以重复使用地址
1 |
|
2.1. 参数取值
__addr
指向要绑定给sockfd的协议地址
1 |
|
二、连接
1. TCP连接
1.1. TCP服务器
1 |
|
1.2. TCP客户端
1 |
2. UDP连接
3. close()和shutdown()的区别
close()
- 头文件
unistd.h
close()
是关闭文件句柄,如果文件句柄没有引用,会找到对应的socket进行关闭清理- 如果存在多个进程共享一个文件句柄,
close()
一个不会断开连接,多个进程都close()
才会断开连接
shutdown()
shutdown()
是关闭socket连接,一个进程关闭连接,另一个进程也无法使用shutdown()
针对的是socket不是文件,关闭socket之后,文件还在,还需要调用close()
才能关闭文件shutdown()
存在第二个参数,控制关闭的方向SHUT_RD
: 只关闭读端,来的数据会丢掉,对方不知道读端关闭SHUT_WR
: 只关闭写端,发送FIN包,对方知道此事情SHUT_RDWR
: 关闭读写端,相当于调用两次,一次指定读,一次是写
1 | // include/linux/net.h |
三、select/poll/epoll
1. select
- windows和linux都存在select
- select可以同时监听多个文件描述符,但是对描述符的处理是轮询的方式,对本地fd池一个一个扫描看是否有数据
- 最大支持
FD_SETSIZE
个文件描述符,一般为1024/2048
2. poll
- 在select基础上去除了文件描述符的限制
- 轮询机制还是保留了
四、epoll
- epoll是linux特有的系统调用,windows没有此实现,使用mingw是模拟的一种实现
- epoll和select区别是,对于fd池的处理,epoll不会一个一个扫描,而是在内核注册了通知机制,当某个fd出现时间,内核会直接通知epoll
- epoll返回的就直接是fd的事件信息,这样防止了扫描带来的开销
1. epoll的调用
1 |
2. epoll下socket是否要设置为非阻塞
- 服务端用于监听的fd,最好使用水平触发模式,边沿触发可能导致部分客户端连接不上
- 和客户端交互的fd,使用水平触发时,阻塞非阻塞都可以,建议设置非阻塞
- 和客户端交互fd,使用边沿触发时,必须使用非阻塞io,否则会卡在读取调用上。但是要求必须一次读完所有数据,否则可能存在数据没有处理。
五、网络编程
1. 头文件
1.1. netinet和linux下的头文件区别
- netinet是用户空间的接口头文件,一般是应用程序使用
- linux下是linux内核使用,一般是内核相关操作使用
2. 一些工具函数
2.1. 网络字节序转换
1 | // #include <netinet/in.h> |
2.2. 网络地址转换 in_addr_t
和char *
1 | // #include <arpa/inet.h> |
示例
1 |
|
3. 分片包处理
主要用到一些宏
1 |
|
4. tcp头处理
1 |
|
5. 获取系统dns服务器
- 下面函数是glibc提供的从
/etc/resolv.conf
文件中读取的dns服务器列表
1 |
|
6. 忽略路由绑定网卡发包
- 方案一
1 |
|
- 方案二
1 |
|
7. 使用非本机ip发送数据包
- 主要使用的是socket的一个选项,
IP_TRANSPARENT
1 | // 设置fd选项为允许透明转发 |
- 发送数据包后,此socket会监听一个非本机ip的地址,而linux本身会在反向路由里面发现非本机地址的数据包会直接丢弃,所以还需要添加策略路由来允许本机接收数据包
1 | 对此ip回包添加mark来匹配策略路由,这样防止发出去的包也被路由处理了 |
六、实战示例
1. tcp传输
- socket确定是TCP协议后,recv和send拿到的数据是tcp数据,不包含tcp头部
1.1. 端口模式
- 服务端
1 |
|
- 客户端
1 |
|
1.2. unix套接字
- 源码分析查看unix套接字
- 服务端,区别仅在init阶段
- 建立
AF_UNIX
套接字的情况下,protocol必须设置为0
1 |
|
- 客户端区别也在init阶段
1 |
|
2. udp传输
- 和tcp不同点在于,服务端不需要listen,调用完bind就可以直接recv
- 客户端不需要connect,创建完socket就可以直接调用
sendto
- 如果对端是一个服务器,但是端口没开放,会回复icmp端口不可达,这个需要通过设置sockopt才能在recvfrom时返回错误,不然就会阻塞
- 如果对端地址不可达,那就无法得知,不会回复icmp不可达的信息
2.2. 客户端代码
1 | int main(int argc, char *argv[]) { |
3. 转移文件句柄到另一个进程
- 主要使用
sendmsg/recvmsg
连个系统调用实现 - 必须使用unix套接字才能发送和接受成功,但是使用tcp协议和udp都可以,域套接字也不会丢包
- 将套接字发送出去后,本进程内的文件句柄还有效可以读写数据,但是关闭套接字不会引起客户端断开连接,即使接收进程没有进行
recvmsg
接受套接字 - 文件描述符发送时,内核会对文件描述符结构体进行拷贝到另一个进程,但是文件描述符句柄的值会根据当前进程的最小未打开的文件描述符进行计算,不会和发送端一致
- 例如发送端发送的fd值是4,接受端受到的会变成其他值,可以是3、4、5等
示例代码
- 发送端
1 |
|
- 接收端
1 | int recvFd(int clientFd) { |
4. 原始socket发包
4.1. 使用原始socket实现一个tcp syn扫描器
带ip头的处理
1 |
|
不带ip头的处理
1 | int main(int argc, char *argv[]) { |
踩坑记
1. TCP连接后,一方断电,另一方是无法检测到对方断电的
- TCP连接并没有检测机制,所以一方断电,另一方无法检测到。
- 强行退出程序,操作系统会回收资源,内部执行关闭套接字,所以客户端可以检测到断开连接。
2. SO_KEEPALIVE 保活
TCP连接选项中有一项是心跳保活机制,但并不是规范的一部分,官方RFC罗列不适用的三个理由:
- 在短暂的故障期间,可能使一个良好的连接被释放
- 占用了不必要的资源
- 在以数据包计费的网络上消耗额外的流量