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