socket
网络基础
典型协议
传输层 常见协议有TCP/UDP协议。
应用层 常见的协议有HTTP协议,FTP协议。
网络层 常见协议有IP协议、ICMP协议、IGMP协议。
网络接口层 常见协议有ARP协议、RARP协议。
TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议。
FTP文件传输协议(File Transfer Protocol)
IP协议是因特网互联协议(Internet Protocol)
ICMP协议是Internet控制报文协议(Internet Control Message Protocol)它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。
IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
ARP协议是正向地址解析协议(Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址。
RARP是反向地址转换协议,通过MAC地址确定IP地址。
网络分层模型结构
1. 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。 2. 数据链路层:定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1 3. 网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。 4. 传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。 5. 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。 6. 表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换。 7. 应用层:是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。 TCP/IP 4层模型:网(链路层/网络接口层)、网、传、应 应用层:http、ftp、nfs、ssh、telnet。。。 传输层:TCP、UDP 网络层:IP、ICMP、IGMP 链路层:以太网帧协议、ARP
c/s模型
client-server
b/s模型
browser-server C/S B/S 优点: 缓存大量数据、协议选择灵活 安全性、跨平台、开发工作量较小 速度快 缺点: 安全性、跨平台、开发工作量较大 不能缓存大量数据、严格遵守 http
网络传输流程
数据没有封装之前,是不能在网络中传递。 数据-》应用层-》传输层-》网络层-》链路层 --- 网络环境
以太网帧协议
ARP协议:根据 Ip 地址获取 mac 地址。 以太网帧协议:根据mac地址,完成数据包传输。
IP协议
版本: IPv4、IPv6 -- 4位 TTL: time to live 。 设置数据包在路由节点中的跳转上限。每经过一个路由节点,该值-1, 减为0的路由,有义务将该数据包丢弃 源IP: 32位。--- 4字节 192.168.1.108 --- 点分十进制 IP地址(string) --- 二进制 目的IP:32位。--- 4字节
IP地址:可以在网络环境中,唯一标识一台主机。
端口号:可以网络的一台主机上,唯一标识一个进程。
ip地址+端口号:可以在网络环境中,唯一标识一个进程。
UDP: 16位:源端口号。 2^16 = 65536
16位:目的端口号。
TCP协议
16位:源端口号。 2^16 = 65536 16位:目的端口号。 32序号; 32确认序号。 6个标志位。 16位窗口大小。 2^16 = 65536
套接字基础
socket套接字
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。) 在通信过程中, 套接字一定是成对出现的。
网络字节序
小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678 大端法:(网络存储) 高位存低地址。地位存高地址。 htonl --> 本地--》网络 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序 htons --> 本地--》网络 (port) ntohl --> 网络--》 本地(IP) ntohs --> 网络--》 本地(Port)
IP地址转换函数
int inet_pton(int af, const char *src, void *dst); 本地字节序(string IP) ---> 网络字节序 af:AF_INET、AF_INET6 src:传入,IP地址(点分十进制) dst:传出,转换后的 网络字节序的 IP地址。 返回值: 成功: 1 异常: 0, 说明src指向的不是一个有效的ip地址。 失败:-1 const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序 ---> 本地字节序(string IP) af:AF_INET、AF_INET6 src: 网络字节序IP地址 dst:本地字节序(string IP) size: dst 的大小。 返回值: 成功:dst。 失败:NULL
sockaddr地址结构
IP + port --> 在网络环境中唯一标识一个进程。
struct sockaddr_in addr; addr.sin_family = AF_INET/AF_INET6 man 7 ip addr.sin_port = htons(9527); int dst; inet_pton(AF_INET, "192.157.22.45", (void *)&dst); addr.sin_addr.s_addr = dst; 【*】addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系统中有效的任意IP地址。二进制类型。 bind(fd, (struct sockaddr *)&addr, size);
socket函数
#include <sys/socket.h> int socket(int domain, int type, int protocol); 创建一个 套接字 domain:AF_INET、AF_INET6、AF_UNIX type:SOCK_STREAM、SOCK_DGRAM protocol: 0 返回值: 成功: 新套接字所对应文件描述符 失败: -1 errno
bind函数
#include <arpa/inet.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个 地址结构 (IP+port) sockfd: socket 函数返回值 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8888); addr.sin_addr.s_addr = htonl(INADDR_ANY); addr: 传入参数(struct sockaddr *)&addr addrlen: sizeof(addr) 地址结构的大小。 返回值: 成功:0 失败:-1 errno
listen函数
int listen(int sockfd, int backlog); 设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量) sockfd: socket 函数返回值 backlog:上限数值。最大值 128. 返回值: 成功:0 失败:-1 errno
accept函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。 sockfd: socket 函数返回值 addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port) socklen_t clit_addr_len = sizeof(addr); addrlen:传入传出。 &clit_addr_len 入:addr的大小。 出:客户端addr实际大小。 返回值: 成功:能与客户端进行数据通信的 socket 对应的文件描述。 失败: -1 , errno
connect函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用现有的 socket 与服务器建立连接 sockfd: socket 函数返回值 struct sockaddr_in srv_addr; // 服务器地址结构 srv_addr.sin_family = AF_INET; srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致。 inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr); addr:传入参数。服务器的地址结构 addrlen:服务器的地址结构的大小 返回值: 成功:0 失败:-1 errno 如果不使用bind绑定客户端地址结构, 采用"隐式绑定".
TCP通信流程
server: 1. socket() 创建socket 2. bind() 绑定服务器地址结构 3. listen() 设置监听上限 4. accept() 阻塞监听客户端连接 5. read(fd) 读socket获取客户端数据 6. 小--大写 toupper() 7. write(fd) 8. close(); client: 1. socket() 创建socket 2. connect(); 与服务器建立连接 3. write() 写数据到 socket 4. read() 读转换后的数据。 5. 显示读取结果 6. close()
TCP协议
状态机
CLOSED****:**表示初始状态。 **LISTEN****:**该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。 **SYN_SENT****:**这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。 **SYN_RCVD:** 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。 **ESTABLISHED****:**表示连接已经建立。 **FIN_WAIT_1:** FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是: FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。 FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。 **FIN_WAIT_2****:主动关闭链接的一方,发出FIN****收到ACK****以后进入该状态****。称之为半连接或半关闭状态。**该状态下的socket只能接收数据,不能发。 **TIME_WAIT:** 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。 **CLOSING:** 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。 **CLOSE_WAIT:** 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。 **LAST_ACK:** 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态。
半关闭
当TCP链接中A发送FIN请求关闭,B端回应ACK后(A端进入FIN_WAIT_2状态),B没有立即发送FIN给A时,A方处在半链接状态,此时A可以接收B发送的数据,但是A已不能再向B发送数据。 从程序的角度,可以使用API来控制实现半连接状态。 \#include <sys/socket.h> int shutdown(int sockfd, int how); sockfd: 需要关闭的socket的描述符 how: 允许为shutdown操作选择以下几种方式: SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。 该套接字**不再接受数据**,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。 SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。 SHUT_RDWR(2): 关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。 使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。 **shutdown****不考虑描述符的引用计数,直接关闭描述符**。也可选择中止一个方向的连接,只中止读或只中止写。 注意: \1. 如果有多个进程共享一个套接字,close每被调用一次,计数减1,直到计数为0时,也就是所用进程都调用了close,套接字将被释放。 \2. 在多进程中如果一个进程调用了shutdown(sfd, SHUT_RDWR)后,其它的进程将无法进行通信。但,如果一个进程close(sfd)将不会影响到其它进程。
三次握手
主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。 被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。 主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带确认序号。
四次挥手
主动关闭连接请求端, 发送 FIN 标志位。 被动关闭连接请求端, 应答 ACK 标志位。 ----- 半关闭完成。 被动关闭连接请求端, 发送 FIN 标志位。 主动关闭连接请求端, 应答 ACK 标志位。 ----- 连接全部关闭
滑动窗口
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
错误处理函数
封装目的: 在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。 存放网络通信相关常用 自定义函数 存放 网络通信相关常用 自定义函数原型(声明)。 命名方式:系统调用函数首字符大写, 方便查看man手册 如:Listen()、Accept(); 函数功能:调用系统调用函数,处理出错场景。 在 server.c 和 client.c 中调用 自定义函数 联合编译 server.c 和 wrap.c 生成 server client.c 和 wrap.c 生成 client
fgets函数
char *fgets(char *str, int n, FILE *stream); str是一个字符数组(或称为字符串),用于存储读取到的文本数据。 n表示要读取的最大字符数(包括换行符和终止符)。 stream是一个指向FILE类型的指针,表示要从中读取数据的文件流,通常是文件指针或stdin(标准输入)。 返回值:fgets函数的返回值是一个指向字符串的指针,即存储读取到的文本数据的字符数组的首地址。如果读取成功,该指针指向传入的字符数组str;如果读取失败或发生了错误,返回值为NULL。
readn/line
readn:读 N 个字节
readline:读一行
read 函数的返回值
1. > 0 实际读到的字节数 2. = 0 已经读到结尾(对端已经关闭)【 !重 !点 !】 3. -1 应进一步判断errno的值: errno = EAGAIN or EWOULDBLOCK: 设置了非阻塞方式 读。 没有数据到达。 errno = EINTR 慢速系统调用被 中断。 errno = “其他情况” 异常。
并发服务器
多进程并发服务器
server.c:
1. Socket(); 创建 监听套接字 lfd 2. Bind() 绑定地址结构 Strcut scokaddr_in addr; 3. Listen(); 4. while (1) { cfd = Accpet(); 接收客户端连接请求。 pid = fork(); if (pid == 0){ 子进程 read(cfd) --- 小-》大 --- write(cfd) close(lfd) 关闭用于建立连接的套接字 lfd read() 小--大 write() } else if (pid > 0) { close(cfd); 关闭用于与客户端通信的套接字 cfd contiue; } } 5. 子进程: close(lfd) read() 小--大 write() 父进程: close(cfd); 注册信号捕捉函数: SIGCHLD 在回调函数中, 完成子进程回收 while (waitpid());
多线程并发服务器
server.c :
1. Socket(); 创建 监听套接字 lfd 2. Bind() 绑定地址结构 Strcut scokaddr_in addr; 3. Listen(); 4. while (1) { cfd = Accept(lfd, ); pthread_create(&tid, NULL, tfn, (void *)cfd); pthread_detach(tid); // pthead_join(tid, void **); 新线程---专用于回收子线程。 } 5. 子线程: void *tfn(void *arg) { // close(lfd) 不能关闭。 主线程要使用lfd read(cfd) 小--大 write(cfd) pthread_exit((void *)10); }
TCP状态时序图
重点记忆: ESTABLISHED、FIN_WAIT_2 <--> CLOSE_WAIT、TIME_WAIT(2MSL) netstat -apn | grep 端口号
2MSL时长
一定出现在【主动关闭连接请求端】。 --- 对应 TIME_WAIT 状态。 保证,最后一个 ACK 能成功被对端接收。(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)
端口复用
int opt = 1; // 设置端口复用。 setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
select多路IO转接
select
-
select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
-
解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,不应在select上投入更多精力
原理: 借助内核, select 来监听, 客户端连接、数据通信事件。
void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。 fd_set rset; FD_ZERO(&rset); void FD_SET(int fd, fd_set *set); --- 将待监听的文件描述符,添加到监听集合中 FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset); void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。 FD_CLR(4, &rset); int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。 返回值: 在:1;不在:0; FD_ISSET(4, &rset); int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout); nfds:监听的所有文件描述符中,最大文件描述符+1 readfds: 读 文件描述符监听集合。 传入、传出参数 writefds:写 文件描述符监听集合。 传入、传出参数 NULL exceptfds:异常 文件描述符监听集合 传入、传出参数 NULL timeout: > 0: 设置监听超时时长。 NULL: 阻塞监听 0: 非阻塞监听,轮询 返回值: > 0: 所有监听集合(3个)中, 满足对应事件的总数。 0: 没有满足监听条件的文件描述符 -1: errno
思路分析
int maxfd = 0; lfd = socket() ; 创建套接字 maxfd = lfd; bind(); 绑定地址结构 listen(); 设置监听上限 fd_set rset, allset; 创建r监听集合 FD_ZERO(&allset); 将r监听集合清空 FD_SET(lfd, &allset); 将 lfd 添加至读集合中。 while(1) { rset = allset; 保存监听集合 ret = select(lfd+1, &rset, NULL, NULL, NULL); 监听文件描述符集合对应事件。 if(ret > 0) { 有监听的描述符满足对应事件 if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。 cfd = accept(); 建立连接,返回用于通信的文件描述符 maxfd = cfd; FD_SET(cfd, &allset); 添加到监听通信描述符集合中。 } for (i = lfd+1; i <= 最大文件描述符; i++){ FD_ISSET(i, &rset) 有read、write事件 read() 小 -- 大 write(); } } }
select优缺点:
缺点: 监听上限受文件描述符限制。 最大 1024. 检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。 优点: 跨平台。win、linux、macOS、Unix、类Unix、mips
poll/epoll
poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:监听的文件描述符【数组】 struct pollfd { int fd: 待监听的文件描述符 short events: 待监听的文件描述符对应的监听事件 取值:POLLIN、POLLOUT、POLLERR short revnets: 传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR } nfds: 监听数组的,实际有效监听个数。 timeout: > 0: 超时时长。单位:毫秒。 -1: 阻塞等待 0: 不阻塞 返回值:返回满足对应监听事件的文件描述符 总个数。 优点: 自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。 拓展 监听上限。 超出 1024限制。 缺点: 不能跨平台。 Linux 无法直接定位满足监听事件的文件描述符, 编码难度较大。
*read 函数返回值
> 0: 实际读到的字节数 =0: socket中,表示对端关闭。close() -1: 如果 errno == EINTR 被异常终端。 需要重启。 如果 errno == EAGIN 或 EWOULDBLOCK 以非阻塞方式读数据,但是没有数据。 需要,再次读。 如果 errno == ECONNRESET 说明连接被 重置。 需要 close(),移除监听队列。
突破 1024 文件描述符限制
cat /proc/sys/fs/file-max --> 当前计算机所能打开的最大文件个数。 受硬件影响。 ulimit -a ——> 当前用户下的进程,默认打开文件描述符个数。 缺省为 1024 修改: 打开 sudo vi /etc/security/limits.conf, 写入: * soft nofile 65536 --> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】 * hard nofile 100000 --> 命令修改上限。
epoll
epoll_create
int epoll_create(int size); 创建一棵监听红黑树 size:创建的红黑树的监听节点数量。(仅供内核参考。) 返回值:指向新创建的红黑树的根节点的 fd。 失败: -1 errno
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作监听红黑树 会将events拷贝到内核中,以供内核使用,所有即使在函数中使用epoll_ctl也不会使ev消失 epfd:epoll_create 函数的返回值。 epfd op:对该监听红黑数所做的操作。 EPOLL_CTL_ADD 添加fd到 监听红黑树 EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。 EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听) fd:待监听的fd ************************************************************************ fd作为client传参时必须使用: ssize_t clilen; clilen=sizeof(cliaddr); ************************************************************************ event: 本质 struct epoll_event 结构体的地址 struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; events: EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭) EPOLLOUT: 表示对应的文件描述符可以写 EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来) EPOLLERR: 表示对应的文件描述符发生错误 EPOLLHUP: 表示对应的文件描述符被挂断; EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 data: 联合体(共用体:只能存储一个变量的值): int fd; 对应监听事件的 fd void *ptr; uint32_t u32; uint64_t u64; 返回值:成功 0; 失败: -1 errno
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 监听满足时间的fd。 epfd:epoll_create 函数的返回值。 epfd events:传出参数,【数组】, 满足监听条件的 哪些 fd 结构体。 maxevents:数组 元素的总个数。 1024 struct epoll_event evnets[1024] timeout: -1: 阻塞 0: 不阻塞 >0: 超时时间 (毫秒) 返回值: > 0: 满足监听的 总个数。 可以用作循环上限。 0: 没有fd满足监听事件 -1:失败。 errno
epoll实现多路IO转接思路
lfd = socket(); 监听连接事件lfd bind(); listen(); int epfd = epoll_create(1024); epfd, 监听红黑树的树根。 struct epoll_event tep, ep[1024]; tep, 用来设置单个fd属性, ep 是 epoll_wait() 传出的满足监听事件的数组。 tep.events = EPOLLIN; 初始化 lfd的监听属性。 tep.data.fd = lfd epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep); 将 lfd 添加到监听红黑树上。 while (1) { ret = epoll_wait(epfd, ep,1024, -1); 实施监听 for (i = 0; i < ret; i++) { if (ep[i].data.fd == lfd) { // lfd 满足读事件,有新的客户端发起连接请求 cfd = Accept(); tep.events = EPOLLIN; 初始化 cfd的监听属性。 tep.data.fd = cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep); } else { cfd 们 满足读事件, 有客户端写数据来。 n = read(ep[i].data.fd, buf, sizeof(buf)); if ( n == 0) { close(ep[i].data.fd); epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL); // 将关闭的cfd,从监听树上摘下。 } else if (n > 0) { 小--大 write(ep[i].data.fd, buf, n); } } }
epoll 事件模型 ET/LT
ET模式: 边沿触发: 缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。 struct epoll_event event; event.events = EPOLLIN | EPOLLET; LT模式: 水平触发 -- 默认采用模式。 缓冲区剩余未读尽的数据会导致 epoll_wait 返回。 结论: epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 --- 忙轮询。 但是非阻塞模式可能会导致数据接收不完全。 //非阻塞 int flg = fcntl(cfd, F_GETFL); flg |= O_NONBLOCK; fcntl(cfd, F_SETFL, flg); struct epoll_event event; event.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event); 优点: 高效。突破1024文件描述符。 缺点: 不能跨平台。 Linux。
epoll 反应堆模型
epoll ET模式 + 非阻塞、轮询 + void *ptr。 原来: socket、bind、listen -- epoll_create创建监听红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)-- -- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回监听满足数组。-- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足 -- read() --- 小->大 -- write回去。 反应堆:不但要监听 cfd 的读事件、还要监听cfd的写事件。 socket、bind、listen -- epoll_create创建监听红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)-- -- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足 -- read() --- 小->大 -- cfd从监听红黑树上摘下 -- EPOLLOUT -- 回调函数 -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听写事件 -- 等待 epoll_wait 返回 -- 说明 cfd 可写 -- write回去 -- cfd从监听红黑树上摘下 -- EPOLLIN -- epoll_ctl() -- EPOLL_CTL_ADD 重新放到红黑上监听读事件 -- epoll_wait 监听 eventset函数: 设置回调函数。 lfd --> acceptconn(); cfd --> recvdata(); cfd --> senddata(); eventadd函数: 将一个fd, 添加到 监听红黑树。 设置监听 read事件,还是监听写事件。 网络编程中: read --- recv() write --- send();
libevent_loop
epoll_event
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
全局变量
23 #define MAX_EVENTS 1024 //监听上限数 24 #define BUFLEN 4096 25 #define SERV_PORT 8080 30/* 描述就绪文件描述符相关信息 */ 31 32 struct myevent_s { 33 int fd; //要监听的文件描述符 34 int events; //对应的监听事件 35 void *arg; //泛型参数 36 void (*call_back)(int fd, int events, void *arg); //回调函数 37 int status; //是否在监听:1->在红黑树上(监听), 0->不在(不监听) 38 char buf[BUFLEN]; 39 int len; 40 long last_active; //记录每次加入红黑树 g_efd 的时间值 41 }; 42 43 int g_efd; //全局变量, 保存epoll_create返回的文件描述符 44 struct myevent_s g_events[MAX_EVENTS+1]; //自定义结构体类型数组. +1-->listen fd
main函数
226 int main(int argc, char *argv[]) 227 { 228 unsigned short port = SERV_PORT; 229 230 if (argc == 2) 231 port = atoi(argv[1]); //atoi字符串转换为int 使用用户指定端口.如未指定,用默认端口 232 233 g_efd = epoll_create(MAX_EVENTS+1); //创建红黑树,返回给全局 g_efd 234 if (g_efd <= 0) 235 printf("create efd in %s err %s\n", __func__, strerror(errno)); 236 237 initlistensocket(g_efd, port); //初始化监听socket 238 239 struct epoll_event events[MAX_EVENTS+1]; //保存已经满足就绪事件的文件描述符数组 240 printf("server running:port[%d]\n", port); 241 242 int checkpos = 0, i; 243 while (1) { 244 /* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */ 245 246 long now = time(NULL); //当前时间 247 for (i = 0; i < 100; i++, checkpos++) { //一次循环检测100个。 使用checkpos控制检测对象 248 if (checkpos == MAX_EVENTS) 249 checkpos = 0; 250 if (g_events[checkpos].status != 1) //不在红黑树 g_efd 上 251 continue; 252 253 long duration = now - g_events[checkpos].last_active; //客户端不活跃的世间 254 255 if (duration >= 60) { 256 close(g_events[checkpos].fd); //关闭与该客户端链接 257 printf("[fd=%d] timeout\n", g_events[checkpos].fd); 258 eventdel(g_efd, &g_events[checkpos]); //将该客户端 从红黑树 g_efd移除 259 } 260 } 261 262 /*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/ 263 int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000); 264 if (nfd < 0) { 265 printf("epoll_wait error, exit\n"); 266 break; 267 } 268 269 for (i = 0; i < nfd; i++) { 270 /*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/ 271 struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr; 272 273 if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) { //读就绪事件 274 ev->call_back(ev->fd, events[i].events, ev->arg); 275 //lfd EPOLLIN
initlistensocket函数
201 /*创建 socket, 初始化lfd */ 200 201 void initlistensocket(int efd, short port) 202 { 203 struct sockaddr_in sin; 204 205 int lfd = socket(AF_INET, SOCK_STREAM, 0); 206 fcntl(lfd, F_SETFL, O_NONBLOCK); //将socket设为非阻塞 207 /*int flag=fcntl(lfd,F_GETFL); flag|=O_NONBLOCK; fcnt(lfd,F_SETFL,flag);*/ 208 memset(&sin, 0, sizeof(sin)); //bzero(&sin, sizeof(sin)) 209 sin.sin_family = AF_INET; 210 sin.sin_addr.s_addr = INADDR_ANY; 211 sin.sin_port = htons(port); 212 213 bind(lfd, (struct sockaddr *)&sin, sizeof(sin)); 214 215 listen(lfd, 20); 216 217 /* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */ 218 eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]); 219 220 /* void eventadd(int efd, int events, struct myevent_s *ev) */ 221 eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); 222 223 return ; 224 }
eventset函数
47 /*将结构体 myevent_s 成员变量 初始化*/ 48 49 void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg) 50 { 51 ev->fd = fd; 52 ev->call_back = call_back; 53 ev->events = 0; 54 ev->arg = arg; 55 ev->status = 0; 56 memset(ev->buf, 0, sizeof(ev->buf)); 57 ev->len = 0; 58 ev->last_active = time(NULL); //调用eventset函数的时间 59 60 return; 61 }
acceptconn函数
103 /* 当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */ 104 105 void acceptconn(int lfd, int events, void *arg) 106 { 107 struct sockaddr_in cin; 108 socklen_t len = sizeof(cin); 109 int cfd, i; 110 111 if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) { 112 if (errno != EAGAIN && errno != EINTR) { 113 /* 暂时不做出错处理 */ 114 } 115 printf("%s: accept, %s\n", __func__, strerror(errno)); //__func__当前函数名称的宏定义 116 return ; 117 } 118 119 do { 120 for (i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素 121 if (g_events[i].status == 0) //类似于select中找值为-1的元素 122 break; //跳出 for 123 124 if (i == MAX_EVENTS) { 125 printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS); 126 break; //跳出do while(0) 不执行后续代码 127 } 128 129 int flag = 0; 130 if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) { //将cfd也设置为非阻塞 131 printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno)); 132 break; 133 } 134 135 /* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */ 136 eventset(&g_events[i], cfd, recvdata, &g_events[i]); 137 eventadd(g_efd, EPOLLIN, &g_events[i]); //将cfd添加到红黑树g_efd中,监听读事件 138 139 } while(0); 140 141 printf("new connect [%s:%d][time:%ld], pos[%d]\n", 142 inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i); 143 return ; 144 }
eventadd函数
63 /* 向 epoll监听的红黑树 添加一个 文件描述符 */ 64 65 //eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); 66 void eventadd(int efd, int events, struct myevent_s *ev) 67 { 68 struct epoll_event epv = {0, {0}}; 69 int op; 70 epv.data.ptr = ev; 71 epv.events = ev->events = events; //EPOLLIN 或 EPOLLOUT 72 73 if (ev->status == 0) { //已经在红黑树 g_efd 里 74 op = EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1 75 ev->status = 1; 76 } 77 78 if (epoll_ctl(efd, op, ev->fd, &epv) < 0) //实际添加/修改 79 printf("event add failed [fd=%d], events[%d]\n", ev->fd, events); 80 else 81 printf("event add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op, events); 82 83 return ; 84 }
eventdel函数
86 /* 从epoll 监听的 红黑树中删除一个 文件描述符*/ 87 88 void eventdel(int efd, struct myevent_s *ev) 89 { 90 struct epoll_event epv = {0, {0}}; 91 92 if (ev->status != 1) //不在红黑树上 93 return ; 94 95 //epv.data.ptr = ev; 96 epv.data.ptr = NULL; 97 ev->status = 0; //修改状态 98 epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv); //从红黑树 efd 上将 ev->fd 摘除 99 100 return ; 101 }
recvdata函数
146 void recvdata(int fd, int events, void *arg) 147 { 148 struct myevent_s *ev = (struct myevent_s *)arg; 149 int len; 150 151 len = recv(fd, ev->buf, sizeof(ev->buf), 0); //读文件描述符, 数据存入myevent_s成员buf中 152 153 eventdel(g_efd, ev); //将该节点从红黑树上摘除 154 155 if (len > 0) { 156 157 ev->len = len; 158 ev->buf[len] = '\0'; //手动添加字符串结束标记 159 printf("C[%d]:%s\n", fd, ev->buf); 160 161 eventset(ev, fd, senddata, ev); //设置该 fd 对应的回调函数为 senddata 162 eventadd(g_efd, EPOLLOUT, ev); //将fd加入红黑树g_efd中,监听其写事件 163 164 } else if (len == 0) { 165 close(ev->fd); 166 /* ev-g_events 地址相减得到偏移元素位置 */ 167 printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events); 168 } else { 169 close(ev->fd); 170 printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno)); 171 } 172 173 return; 174 }
senddata函数
176 void senddata(int fd, int events, void *arg) 177 { 178 struct myevent_s *ev = (struct myevent_s *)arg; 179 int len; 180 181 len = send(fd, ev->buf, ev->len, 0); //直接将数据 回写给客户端。未作处理 182 183 eventdel(g_efd, ev); //从红黑树g_efd中移除 184 185 if (len > 0) { 186 187 printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf); 188 eventset(ev, fd, recvdata, ev); //将该fd的 回调函数改为 recvdata 189 eventadd(g_efd, EPOLLIN, ev); //从新添加到红黑树上, 设为监听读事件 190 191 } else { 192 close(ev->fd); //关闭链接 193 printf("send[fd=%d] error %s\n", fd, strerror(errno)); 194 } 195 196 return ; 197 }
udp&本地套接字
ctag
递归地索引当前目录及其子目录下的所有源代码文件,并生成一个名为 tags
的标签文件。
ctags -R .
-
-
定位到代码定义处:在 Vim 中,将光标移动到某个标识符(如函数名、变量名)上,按下
Ctrl+]
键,Vim 会自动查找并跳转到该标识符的定义处。如果有多个定义处,Vim 会列出这些选项供你选择。 -
返回上一处位置:如果你想返回到之前的位置(跳转前的位置),可以使用
Ctrl+T
键返回。
-
-
其他 ctags 命令:
-
列出所有标签:在 Vim 中,执行
:tag
命令会列出当前标签文件中的所有标签。 -
跳转到指定标签:在 Vim 中,执行
:tag <tagname>
命令会跳转到指定标签名的定义处。 -
查找标签:在 Vim 中,执行
:tselect <tagname>
命令会在当前标签文件中查找指定标签名,并在 Quickfix 窗口中列出所有匹配项。
-
线程池
全局变量
#define DEFAULT_TIME 10 /*10s检测一次*/ #define MIN_WAIT_TASK_NUM 10 /*如果queue_size > MIN_WAIT_TASK_NUM 添加新的线程到线程池*/ #define DEFAULT_THREAD_VARY 10 /*每次创建和销毁线程的个数*/ #define true 1 #define false 0 struct threadpool_t { pthread_mutex_t lock; /* 用于锁住本结构体 */ pthread_mutex_t thread_counter; /* 记录忙状态线程个数的琐 -- busy_thr_num */ pthread_cond_t queue_not_full; /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */ pthread_cond_t queue_not_empty; /* 任务队列里不为空时,通知等待任务的线程 */ pthread_t *threads; /* 存放线程池中每个线程的tid。数组 */ pthread_t adjust_tid; /* 存管理线程tid */ threadpool_task_t *task_queue; /* 任务队列(数组首地址) */ int min_thr_num; /* 线程池最小线程数 */ int max_thr_num; /* 线程池最大线程数 */ int live_thr_num; /* 当前存活线程个数 */ int busy_thr_num; /* 忙状态线程个数 */ int wait_exit_thr_num; /* 要销毁的线程个数 */ int queue_front; /* task_queue队头下标 */ int queue_rear; /* task_queue队尾下标 */ int queue_size; /* task_queue队中实际任务数 */ int queue_max_size; /* task_queue队列可容纳任务数上限 */ int shutdown; /* 标志位,线程池使用状态,true或false */ }; typedef struct { void *(*function)(void *); /* 函数指针,回调函数 */ void *arg; /* 上面函数的参数 */ } threadpool_task_t; /* 各子线程任务结构体 */
线程池模块分析
1. main(); 创建线程池。 向线程池中添加任务。 借助回调处理任务。 销毁线程池。 2. pthreadpool_create(); 创建线程池结构体 指针。 初始化线程池结构体 { N 个成员变量 } 创建 N 个任务线程。 创建 1 个管理者线程。 失败时,销毁开辟的所有空间。(释放) 3. threadpool_thread() 进入子线程回调函数。 接收参数 void *arg --》 pool 结构体 加锁 --》lock --》 整个结构体锁 判断条件变量 --》 wait -------------------170 4. adjust_thread() 循环 10 s 执行一次。 进入管理者线程回调函数 接收参数 void *arg --》 pool 结构体 加锁 --》lock --》 整个结构体锁 获取管理线程池要用的到 变量。 task_num, live_num, busy_num 根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。 5. threadpool_add () 总功能: 模拟产生任务。 num[20] 设置回调函数, 处理任务。 sleep(1) 代表处理完成。 内部实现: 加锁 初始化 任务队列结构体成员。 回调函数 function, arg 利用环形队列机制,实现添加任务。 借助队尾指针挪移 % 实现。 唤醒阻塞在 条件变量上的线程。 解锁 6. 从 3. 中的wait之后继续执行,处理任务。 加锁 获取 任务处理回调函数,及参数 利用环形队列机制,实现处理任务。 借助队头指针挪移 % 实现。 唤醒阻塞在 条件变量 上的 server。 解锁 加锁 改忙线程数++ 解锁 执行处理任务的线程 加锁 改忙线程数—— 解锁 7. 创建 销毁线程 管理者线程根据 task_num, live_num, busy_num 根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。 如果满足 创建条件 pthread_create(); 回调 任务线程函数。 live_num++ 如果满足 销毁条件 wait_exit_thr_num = 10; signal 给 阻塞在条件变量上的线程 发送 假条件满足信号 跳转至 --170 wait阻塞线程会被 假信号 唤醒。判断: wait_exit_thr_num > 0 pthread_exit();
main函数
int main(void){ /*threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size);*/ threadpool_t *thp = threadpool_create(3,100,100); /*创建线程池,池里最小3个线程,最大100,队列最大100*/ printf("pool inited"); //int *num = (int *)malloc(sizeof(int)*20); int num[20], i; for (i = 0; i < 20; i++) { num[i] = i; printf("add task %d\n",i); /*int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg) */ threadpool_add(thp, process, (void*)&num[i]); /* 向线程池中添加任务 */ } sleep(10); /* 等子线程完成任务 */ threadpool_destroy(thp); return 0; }
TCP通信VSUDP通信
TCP:面向连接的,可靠的数据包传输。对于不稳定的网络层,采取完全弥补的通信方式:丢包重传。 优点: 稳定。 数据流量稳定、速度稳定、顺序 缺点: 传输速度慢。相率低。开销大。 使用场景:数据的完整性要求较高,不追求效率。 大数据传输、文件传输。
UDP:无连接的,不可靠的数据报传递。对于不稳定的网络层,采取完全不弥补的通信方式:默认还原网络状况 优点: 传输速度快。相率高。 开销小。 缺点: 不稳定。 数据流量。速度。顺序。 使用场景:对时效性要求较高场合。稳定性其次。 游戏、视频会议、视频电话。 腾讯、华为、阿里 --- 应用层数据校验协议,弥补udp的不足。
recv函数
ssize_t recv(int sockfd, void *buf, size_t len, int flags); sockfd:套接字 buf:缓冲区地址 len:缓冲区大小 flags: 0 返回值: 成功:接收数据字节数。 失败:-1 errn。 0: 对端关闭。
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); sockfd: 套接字 struct iovec { /* Scatter/gather array items */ void *iov_base; 数据的开始地址 size_t iov_len; 数据的字节数 }; struct msghdr { void *msg_name; 目标地址 socklen_t msg_namelen; 地址大小 struct iovec *msg_iov; /* Scatter/gather array */ size_t msg_iovlen; 数组中元素数量 void *msg_control; 辅助数据 size_t msg_controllen; 辅助数据缓冲区长度 int msg_flags; 收到消息的标志位 }; 返回值: 成功:接收数据字节数。 失败:-1 errn。 0: 对端关闭。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); sockfd: 套接字 buf:缓冲区地址 len:缓冲区大小 flags: 0 src_addr:(struct sockaddr *)&addr 传出。 对端地址结构 addrlen:传入传出。 返回值: 成功:接收数据字节数。 失败:-1 errn。 0: 对端关闭。
send函数
ssize_t send(int sockfd, const void *buf, size_t len, int flags); sockfd: 套接字 buf:存储数据的缓冲区 len:数据长度 flags: 0 返回值:成功:写出数据字节数。 失败 -1, errno
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); sockfd: 套接字 flags: 0 struct iovec { /* Scatter/gather array items */ void *iov_base; 数据的开始地址 size_t iov_len; 数据的字节数 }; struct msghdr { void *msg_name; 目标地址 socklen_t msg_namelen; 地址大小 struct iovec *msg_iov; /* Scatter/gather array */ size_t msg_iovlen; 数组中元素数量 void *msg_control; 辅助数据 size_t msg_controllen; 辅助数据缓冲区长度 int msg_flags; 收到消息的标志位 }; 返回值:成功:写出数据字节数。 失败 -1, errno
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen); sockfd: 套接字 buf:存储数据的缓冲区 len:数据长度 flags: 0 src_addr:(struct sockaddr *)&addr 传入。 目标地址结构 addrlen:地址结构长度。 返回值:成功:写出数据字节数。 失败 -1, errno
UDP实现的 C/S 模型
不需要多进,线程,select,epoll就实现并发。
recv()/send() 只能用于 TCP 通信。 替代 read、write accpet(); ---- Connect(); ---被舍弃 server: lfd = socket(AF_INET, STREAM, 0); SOCK_DGRAM --- 报式协议。 listen(); --- 可有可无 bind(); while(1){ read(cfd, buf, sizeof) --- 被替换 --- recvfrom() --- 涵盖accept传出地址结构。 recvfrom(); 小-- 大 write();--- 被替换 --- sendto()---- connect sendto(); } close(); client: connfd = socket(AF_INET, SOCK_DGRAM, 0); sendto(‘服务器的地址结构’, 地址结构大小) recvfrom() 写到屏幕 close();
本地套接字
IPC: pipe、fifo、mmap、信号、本地套接字(domain)--- CS模型
对比网络编程 TCP C/S模型, 注意以下几点: 1. int socket(int domain, int type, int protocol); 参数 domain:AF_INET --> AF_UNIX/AF_LOCAL type: SOCK_STREAM/SOCK_DGRAM 都可以。 2. 地址结构: sockaddr_in --> sockaddr_un struct sockaddr_in srv_addr; --> struct sockaddr_un srv_adrr; srv_addr.sin_family = AF_INET; --> srv_addr.sun_family = AF_UNIX; srv_addr.sin_port = htons(8888); strcpy(srv_addr.sun_path, "srv.socket") srv_addr.sin_addr.s_addr=htonl(INADDR_ANY); len=offsetof(struct sockaddr_un, sun_path)+strlen("srv.socket"); bind(fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)); bind(fd, (struct sockaddr *)&srv_addr, len); 3. bind()函数调用成功,会创建一个 socket。因此为保证bind成功,通常我们在 bind之前, 可以使用 unlink("srv.socket"); 4. 客户端不能依赖 “隐式绑定”。并且应该在通信建立过程中,创建且初始化2个地址结构: 1) client_addr --> bind() 2) server_addr --> connect();
offsetof
size_t offsetof(type, member);从结构类型的开头返回字段成员的偏移量。
网络套接字VS本地套接字
网络套接字 本地套接字 server: lfd = socket(AF_INET, SOCK_STREAM, 0); lfd = socket(AF_UNIX, SOCK_STREAM, 0); bzero() ---- struct sockaddr_in serv_addr; bzero() - struct sockaddr_un serv_addr, clie_addr; serv_addr.sin_family = AF_INET; serv_addr.sun_family = AF_UNIX; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); strcpy(serv_addr.sun_path, "套接字文件名") serv_addr.sin_port = htons(8888); len = offsetof(sockaddr_un, sun_path) + strlen(); bind(lfd, (struct sockaddr *)&serv_addr, sizeof()); unlink("套接字文件名"); bind(lfd, (struct sockaddr *)&serv_addr, len); 创建新文件 Listen(lfd, 128); Listen(lfd, 128); cfd = Accept(lfd, ()&clie_addr, &len); cfd = Accept(lfd, ()&clie_addr, &len); client: lfd = socket(AF_INET, SOCK_STREAM, 0); lfd = socket(AF_UNIX, SOCK_STREAM, 0); " 隐式绑定 IP+port" bzero() ---- struct sockaddr_un clie_addr; clie_addr.sun_family = AF_UNIX; strcpy(clie_addr.sun_path, "client套接字文件名") len = offsetof(sockaddr_un, sun_path) + strlen(); unlink( "client套接字文件名"); bind(lfd, (struct sockaddr *)&clie_addr, len); bzero() ---- struct sockaddr_in serv_addr; bzero() ---- struct sockaddr_un serv_addr; serv_addr.sin_family = AF_INET; serv_addr.sun_family = AF_UNIX; inet_pton(AF_INT, "服务器IP", &sin_addr.s_addr) strcpy(serv_addr.sun_path, "server套接字文件名") serv_addr.sin_port = htons("服务器端口"); len = offsetof(sockaddr_un, sun_path) + strlen(); connect(lfd, &serv_addr, sizeof()); connect(lfd, &serv_addr, len);
libevent
libevent例子路径:/usr/share/doc/libevent-dev/examples
libevent库
开源。精简。跨平台(Windows、Linux、maxos、unix)。专注于网络通信。
源码包安装: 参考 README、readme
./configure 检查安装环境 生成 makefile make 生成 .o 和 可执行文件 sudo make install 将必要的资源cp置系统指定目录。 进入 sample 目录,运行demo验证库安装使用情况。 编译使用库的 .c 时,需要加 -levent 选项。 库名 libevent.so --> /usr/local/lib 查看的到。
特性: 基于“事件”异步通信模型。--- 回调。
libevent框架
1. 创建 event_base (乐高底座) struct event_base *event_base_new(void); struct event_base *base = event_base_new(); 2. 创建事件evnet 常规事件 event --> event_new(); bufferevent --> bufferevent_socket_new(); 3. 将事件添加到 base上 int event_add(struct event *ev, const struct timeval *tv) 4. 循环监听事件满足 int event_base_dispatch(struct event_base *base); 5. 释放 event_base event_base_free(base);
event_new
struct event *ev; struct event *event_new(struct event_base *base,evutil_socket_t fd,short what,event_callback_fn cb; void *arg); base: event_base_new()返回值。 fd: 绑定到 event 上的 文件描述符 what:对应的事件(r、w、e) EV_READ 一次读事件 EV_WRITE 一次写事件 EV_PERSIST 持续触发。 结合 event_base_dispatch 函数使用,生效。 cb:一旦事件满足监听条件,回调的函数。 typedef void (*event_callback_fn)(evutil_socket_t fd, short, void *) arg: 回调的函数的参数。 返回值:成功创建的 event
event_add
int event_add(struct event *ev, const struct timeval *tv); ev: event_new() 的返回值。 tv:NULL
event_del
int event_del(struct event *ev); ev: event_new() 的返回值。
event_free
int event_free(struct event *ev); ev: event_new() 的返回值。
未决和非未决
非未决: 没有资格被处理 未决: 有资格被处理,但尚未被处理 event_new --> event ---> 非未决 --> event_add --> 未决 --> dispatch() && 监听事件被触发 --> 激活态 --> 执行回调函数 --> 处理态 --> 非未决 event_add && EV_PERSIST --> 未决 --> event_del --> 非未决
bufferevent
#include <event2/bufferevent.h> read/write 两个缓冲. 借助 队列. 只能读一次 FIFO
创建、销毁基于套接字的bufferevent
struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options); base: event_base fd: 封装到bufferevent内的fd options:BEV_OPT_CLOSE_ON_FREE 返回: 成功创建的 bufferevent事件对象。 void bufferevent_socket_free(struct bufferevent *ev);
给bufferevent设置回调
event: event_new( fd, callback ); event_add() -- 挂到 event_base 上。 bufferevent:bufferevent_socket_new(fd) bufferevent_setcb( callback ) void bufferevent_setcb(struct bufferevent * bufev, bufferevent_data_cb readcb, bufferevent_data_cb writecb, bufferevent_event_cb eventcb, void *cbarg ); 参数:readcb: 设置 bufferevent 读缓冲,对应回调 read_cb{ bufferevent_read() 读数据 } writecb: 设置 bufferevent 写缓冲,对应回调 write_cb { } -- 给调用者,发送写成功通知。 可以 NULL eventcb: 设置 事件回调。 也可传NULL cbarg: 上述回调函数使用的 参数。 read 回调函数类型: typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void*ctx); void read_cb(struct bufferevent *bev, void *cbarg ) { ..... bufferevent_read(); --- read(); } bufferevent_read()函数的原型:从缓冲区读。 size_t bufferevent_read(struct bufferevent *bev, void *buf, size_t bufsize); write 回调函数类型: void read_cb(struct bufferevent *bev, void *cbarg ) { ..... bufferevent_write(); --- write(); } int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size); 写到缓冲区 event回调函数: typedef void (*bufferevent_event_cb)(struct bufferevent *bev, short events, void *ctx); void event_cb(struct bufferevent *bev, short events, void *ctx) events: BEV_EVENT_CONNECTED
启动/关闭bufferevent的缓冲区
void bufferevent_enable(struct bufferevent *bufev, short events); 启动 void bufferevent_disable(struct bufferevent *bufev, short events); 关闭 events: EV_READ、EV_WRITE、EV_READ|EV_WRITE 默认、write 缓冲是 enable、read 缓冲是 disable bufferevent_enable(evev, EV_READ); -- 开启读缓冲。
连接客户端
socket();connect(); int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen); bev: bufferevent 事件对象(封装了fd) address、len:等同于 connect() 参2/3
创建监听服务器
------ socket();bind();listen();accept();----- struct evconnlistener * listener struct evconnlistener *evconnlistener_new_bind ( struct event_base *base, evconnlistener_cb cb, void *ptr, unsigned flags, int backlog, const struct sockaddr *sa, int socklen); base: event_base cb: 回调函数。 一旦被回调,说明在其内部应该与客户端完成, 数据读写操作,进行通信。 ptr: 回调函数的参数 flags: LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE backlog: listen() 2参。 -1 表最大值 sa:服务器自己的地址结构体 socklen:服务器自己的地址结构体大小。 返回值:成功创建的监听器。
链接监听器回调
typedef void (*evconnlistener_cb)(struct evconnlistener *listener, evutil_socket_t sock, struct sockaddr *addr, int len, void *ptr); 接收到新连接会调用提供的回调函数 。 listener 参数是接收连接的连接监听器 。 sock 参数是新接收的套接字fd。 addr 和 len 参数是接收连接的地址和地址长度。 ptr 是调 用 evconnlistener_new() 时用户提供的指针。
释放监听服务器
void evconnlistener_free(struct evconnlistener *lev);
libevent 创建TCP连接
服务器端: 1. 创建event_base 2. 使用 evconnlistener_new_bind 创建监听服务器, 设置其回调函数,当有客户端成功连接时,这个回调函数会被调用。 3. 在evconnlistener_new_bind的回调函数里,处理链接后的操作 4. 回调函数被调用,说明有客户端连接,得到fd,用于和客户端通信 5. 创建bufferevent事件对象。bufferevent_socket_new();将fd封装到这个对象中 6. 使用bufferevent_setcb() 函数给 bufferevent的 read、write、event 设置回调函数。 7. 设置读缓冲、写缓冲的使能状态 enable、disable //半关闭 8. 封装readcb,writecb,eventcb。接受,发送数据。bufferevent_read()/bufferevent_write() 9. 启动循环 event_base_dispath(); 10. 释放连接。 客户端: 1. 创建event_base 2. 使用bufferevent_socket_new创建一个与服务器通信的bufferevent时间 3. 使用bufferevent_socket_connect连接服务器 4. 使用bufferevent_setcb设置read,write,event回调 5. 设置bufferevent的读写缓冲区(enable/disable) 6. 接受/发送数据 bufferevent_read();bufferevent_write(); 7. 释放资源