Linux网络编程
2024-06-18 14:46:09 2 举报
AI智能生成
Linux后端
作者其他创作
大纲/内容
应用程序通过网络协议进行通信的接口,是应用程序与网络协议根进行交互的接口
与管道类似的,Linux 系统将其封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致
区别是管道主要应用于本地进程间通信,而套接字多应用于网络进程间数据的传递。
在 Linux 环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
简介
被动接受连接,一般不会主动发起连接
服务器端
主动向服务器发起连接
客户端
套接字通信
Socket
现代 CPU 的累加器一次都能装载(至少)4 字节(这里考虑 32 位机),即一个整数
这 4字节在内存中排列的顺序将影响它被累加器装载成的整数的值,这就是字节序问题
在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送
低地址存高字节
大端字节序
这里04是第字节,01是高字节
高地址存高字节
小端字节序
分类
当格式化的数据在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解释之
发送端总是把要发送的数据转换成大端字节序数据后再发送
而接收端知道对方传送过来的数据总是采用大端字节序
所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换((小端机转换,大端机不转换))
解决方式
字节序转换函数
TCP/IP 中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关
网络字节顺序采用大端排序方式
网络字节顺序
字节序转换接口
字节序
socket地址其实是一个结构体,封装端口号和IP等信息
后面的socket相关的api中需要使用到这个socket地址
#include <bits/socket.h>struct sockaddr {sa_family_t sa_family;char sa_data[14];};typedef unsigned short int sa_family_t;
是地址族类型(sa_family_t)的变量
地址族类型通常与协议族类型对应
宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
常见协议族对应地址族关系
sa_family
用于存放 socket 地址值
不同的协议族的地址值具有不同的含义和长度
sa_data
结构体 sockaddr
由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的
结构体sockaddr_storage
通用socket地址
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体
为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数
至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr
专用 socket 地址
socket地址
通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址
但编程中我们需要先把它们转化为整数(二进制数)方能使用
span style=\
IPv4地址和网络字节序整数表示的IPv4地址间的转换
更广泛适用
函数
IP地址转换
监听:监听有客户端的连接
创建一个用于监听的套接字
客户端连接服务器的时候使用的就是这个IP和端口
将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
设置监听,监听的fd开始工作
阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
接收数据
发送数据
通信
通信结束,断开连接
创建一个用于通信的套接字(fd)
连接服务器,需要指定连接的服务器的 IP 和 端口
连接成功了,客户端可以直接和服务器通信
TCP通信流程
setsockopt
函数原型
创建一个套接字
功能
AF_INET : ipv4
AF_INET6 : ipv6
domain: 协议族
SOCK_STREAM : 流式协议(TCP为代表)
SOCK_DGRAM : 报式协议(UDP为代表)
type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议默认使用 TCP
SOCK_DGRAM : 报式协议默认使用 UDP
protocol具体的一个协议。一般写0,此时系统会根据type进行指定
参数
成功:返回文件描述符,操作的就是内核缓冲区。
失败:-1
返回值
socket()
绑定,将fd 和本地的IP + 端口进行绑定
sockfd : 通过socket函数得到的文件描述符
addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
addrlen : 第二个参数结构体占的内存大小
成功0
失败-1
bind()
监听这个socket上的连接
sockfd : 通过socket()函数得到的文件描述符
backlog : 未连接的和已经连接的和的最大值
listen()
sockfd : 用于监听的文件描述符
addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
accept()
客户端连接服务器
sockfd : 用于通信的文件描述符
addrlen : 第二个参数的内存大小
connect()
sockfd:已连接的套接字描述符
buf:指向要发送数据的缓冲区的指针
len:要发送的数据的字节数
flags:指定发送方式的标志。通常设置为0。
成功时,返回实际发送的字节数
失败时,返回-1
send()
buf:指向用于存储接收数据的缓冲区的指针
len:缓冲区的大小,表示可以接收的最大字节数
flags:指定接收方式的标志。通常设置为0
成功时,返回实际接收的字节数
如果连接已关闭,返回0
recv()
通信函数
使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为 0 时才关闭连接
shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读或只中止写。
作用
sockfd: 需要关闭的socket的描述符
关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。
SHUT_RD(0)
关闭sockfd的写功能,此选项将不允许sockfd进行写操作
SHUT_WR(1)
关闭sockfd的读写功能。
SHUT_RDWR(2):
how: 允许为shutdown操作选择以下几种方式:
如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用进程都调用了 close,套接字将被释放。
注意
shutdown()
关闭套接字
sockfd:要设置选项的套接字的文件描述符
SOL_SOCKET(通用套接字选项)
IPPROTO_IP(IPv4选项)
IPPROTO_IPV6(IPv6选项)
IPPROTO_TCP(TCP选项)
level:选项所在的协议层
SO_REUSEADDR
SO_KEEPALIVE
... ...
要设置的选项的名称
optname
指向包含新选项值的缓冲区的指针
optval
optval缓冲区的大小
optlen
参数说明
成功时返回0
失败时返回-1,并设置errno以指示错误
setsockopt()
设置套接字
套接字函数
防止服务器重启时之前绑定的端口还未释放
程序突然退出而系统没有释放端口
这是一个套接字选项,允许多个套接字绑定到同一个端口上,只要它们绑定的IP地址不同(多网卡设备)
在开发测试场景中,可能会遇到频繁重启的情况,这个时候如果要等TIMEWAIT时间过去,可能会导致测试出错
它还允许一个新的套接字绑定到一个处于TIME_WAIT状态的端口上这在服务器重启时特别有用,因为它允许服务器立即重新绑定到之前使用的端口上,而不必等待TIME_WAIT状态过去
允许多个套接字绑定到完全相同的IP地址和端口上,前提是这些套接字都设置了这个选项
当你想要在多个进程或线程之间分配传入的连接时,可以启用这个选项
也可以实现简单的负载均衡,因为内核会确保每个套接字都接收到大约相同数量的传入连接
SO_REUSEPORT
套接字选项
端口复用
使得程序能同时监听多个文件描述符
目的
首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中
该函数是阻塞的
函数对文件描述符的检测操作是由内核完成的
调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O操作时,该函数才返回。
在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
思想
select使用三个位集合(读、写、异常)来表示要监视的文件描述符
当调用select函数时,内核会遍历所有的文件描述符,查看哪些描述符满足条件
实现原理
nfds : 委托内核检测的最大文件描述符的值 + 1
要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
一般检测读操作
对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲
是一个传入传出参数
readfds
要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
委托内核检测写缓冲区是不是还可以写数据(不满的就可以写)
writefds
exceptfds检测发生异常的文件描述符的集合
timeout设置的超时时间
成功>0(n)检测的集合中有n个文件描述符发生了变化
select()
将参数文件描述符fd对应的标志位设置为0
判断fd对应的标志位是0还是1
返回值fd对应的标志位的值,0,返回0, 1,返回1
将参数文件描述符fd 对应的标志位,设置为1
void FD_ZERO(fd_set *set);
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
字段
NULL : 永久阻塞,直到检测到了文件描述符有变化
tv_sec = 0 tv_usec = 0, 不阻塞
tv_sec > 0 tv_usec > 0, 阻塞对应的时间
设置
timeval结构体
相关数据结构
跨平台,几乎所有的系统都支持
优点
对于readfds集合:select会清除那些不可读的文件描述符,只保留那些有数据可读或已经到达文件末尾的文件描述符
对于writefds集合:select会清除那些不可写的文件描述符,只保留那些可以无阻塞地写入数据的文件描述符。
对于exceptfds集合:select会清除那些没有异常的文件描述符,只保留那些有异常条件的文件描述符(例如“带外”数据到达的套接字)。
每次调用select后,会修改传入的文件描述符集合,使其只包含状态发生变化的文件描述符
文件描述符数量受限(通常是1024)
效率不高,尤其是在文件描述符数量很大时
缺点
优缺点
适用于文件描述符数量较少,且不需要高并发的场景。
适用场景
select
和select类似,但是使用结构数组表示文件操作符
poll与select类似,但使用结构数组来表示文件描述符,而不是位集合
调用poll函数时,内核会遍历所有的pollfd结构,查看哪些描述符满足条件
fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合
nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1
0 : 不阻塞
-1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
>0 : 阻塞的时长
timeout : 阻塞时长
成功n>0,n表示检测到集合中有n个文件描述符发生变化
poll()
委托内核检测的文件描述符
int fd;
委托内核检测文件描述符的什么事件
short events
文件描述符实际发生的事件,传出参数
short revents;
pollfd
没有最大文件描述符的限制
当监视的文件描述符数量很大时,效率仍然不高
poll
使用事件驱动方式,只返回活跃的文件描述符,而不是检查所有文件描述符。
允许用户注册一个文件描述符集合,并只通知用户集合中的文件描述符状态发生变化的情况
使用了数据结构如红黑树来管理文件描述符,使得文件描述符的插入、删除和查找都非常高效
当文件描述符的状态发生变化时,它们会被添加到一个就绪链表中,这使得epoll能够快速地获取到活跃的文件描述符
底层原理
创建一个epoll实例,返回一个文件描述符
int epoll_create(int size);
此参数不再重要,因为在现代的Linux内核中,它不影响epoll实例的大小
但为了向后兼容,你仍然需要为它提供一个大于0的值
size
成功时返回一个非负整数,表示epoll实例的文件描述符
失败时返回-1,并设置errno
epoll_create
用于添加、修改或删除要被epoll实例监视的文件描述符
epfd由epoll_create返回的epoll实例的文件描述符
EPOLL_CTL_ADD:注册新的文件描述符到epfd
EPOLL_CTL_MOD:修改epfd中的文件描述符
EPOLL_CTL_DEL:从epfd中删除一个文件描述符
op:操作类型,可以是以下值之一
fd:要操作的文件描述符
event:指向epoll_event结构体的指针,描述fd上的感兴趣的事件和其他信息。
epoll_ctl
等待epfd上的文件描述符之一变得活跃。
epfd:由epoll_create返回的epoll实例的文件描述符
events:返回活跃文件描述符的事件数组
maxevents:events数组可以容纳的最大事件数
timeout:等待的最大时间(以毫秒为单位)。-1表示无限等待,0表示立即返回
成功时返回活跃的文件描述符的数量
如果超时,返回0
epoll_wait
这是一个位掩码,用于指定感兴趣的事件或接收活跃的事件
表示对应的文件描述符可以读(例如数据在套接字缓冲区中等待被读取)。
EPOLLIN
表示对应的文件描述符可以写
EPOLLOUT
表示对应的文件描述符有紧急的数据可读(这通常指的是TCP套接字的带外数据)
EPOLLPRI
表示对应的文件描述符发生了错误
EPOLLERR
表示对应的文件描述符被挂断
EPOLLHUP
设置为边缘触发(Edge Triggered)模式,这是与默认的水平触发(Level Triggered)模式相对的
EPOLLET
表示一旦某个事件被检测到,该文件描述符会被epoll暂时从跟踪列表中移除,直到应用程序再次调用epoll_ctl修改其状态
EPOLLONESHOT
常见的事件类型包括
uint32_t events;
这是一个联合体,允许用户存储与文件描述符相关的数据。它可以是一个指针、一个整数或者一个文件描述符。
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64;} epoll_data_t;
详细定义
这个字段对于应用程序来说非常有用,因为它允许你与每个文件描述符关联额外的信息,这在事件发生时可以被快速地检索出来
epoll_data_t data;
epoll_event
高效:即使在大量文件描述符的情况下,epoll也能保持很高的效率
可扩展性:与文件描述符的数量几乎无关,非常适合高并发场景
边缘触发(ET)和水平触发(LT)两种模式:为不同的应用场景提供灵活性
Linux特有,不具备跨平台性
使用不当可能会导致复杂的bug,例如在ET模式下
高并发服务器:如Web服务器、数据库服务器等,它们需要同时处理大量的客户端连接。
实时应用:需要快速响应外部事件的应用,如游戏服务器、交易系统等
应用场景
当文件描述符准备好进行某种I/O操作(例如,数据可读或可写)时,LT模式会持续通知应用程序,直到应用程序处理了该事件
工作原理
如果你没有读取或写入所有的数据,每次调用epoll_wait都会返回该文件描述符,因为它仍然是活跃的
它更容易理解和使用,因为你不必担心可能错过事件
特点
当你希望每次事件发生时都得到通知,或者当你不确定何时处理事件时,LT是一个好选择
水平触发
只有当文件描述符的状态从“未准备好”变为“准备好”时,ET模式才会通知应用程序
一旦应用程序被通知,它必须处理所有的数据,因为它不会再次被通知,直到下一次状态变化
你可能只会得到一次通知,即使数据仍然可用。因此,你可能需要在一个循环中读取或写入数据,直到操作返回EAGAIN或EWOULDBLOCK
它可能更高效,因为系统不会因为同一个事件反复通知你
当你希望减少系统调用的数量并自己管理I/O操作时,ET是一个好选择
边缘触发
边缘触发(ET)和水平触发(LT)
在使用边缘触发(ET)模式时,需要确保非阻塞地读/写数据,直到EAGAIN错误,以确保不会错过任何事
epoll的文件描述符自身也可以被添加到epoll的监视列表中,这样当其他文件描述符准备好时,这个epoll的文件描述符也会变得活跃
epoll
类型
系统在没有事件发生时处于空闲或等待状态
当事件发生时,对应的事件处理器或回调函数被触发
常用于图形用户界面、网络编程、异步I/O等场景
高效:可以处理大量并发操作,而不需要为每个操作分配独立的线程或进程
响应快:能够迅速响应外部事件
编程复杂性可能增加,特别是在需要处理大量不同事件的应用中
事件驱动模型
系统定期检查条件是否满足,而不是等待事件发生
适用于事件发生频率较高的场景
缺点是可能浪费CPU时间在无效的检查上
轮询驱动
当外部条件满足时,硬件会产生中断,导致CPU停止当前任务并执行中断处理程序
常用于低级硬件编程,如设备驱动
优点是实时性高,只在需要时响应
中断驱动
当某个条件满足时,系统会发送一个信号到目标进程,进程可以预先定义对特定信号的响应
信号驱动
编程范式
IO多路复用
img src=\
TCP通讯流程图
Linux网络编程
0 条评论
回复 删除
下一页