文章目录
详细的计网知识:
Socket编程:
常见的协议:
套接字概念:
Socket
,在Linux环境下用于表示进程间网络通信的特殊文件类型,本质是内核借助缓冲区形成的伪文件。- 与管道类似,读写套接字和读写文件操作一致。区别:管道主要应用于本地进程间通信,套接字多用于网络进程间通信。
- 套接字内核实现较为复杂,有待进一步学习。。。
- 在
TCP/IP
协议中,IP+TCP/UDP端口
可唯一标识网络通信中的一个进程,也就唯一对应一个socket
。
- 如果要在两个进程间网络通信,则需要一对
socket
,即可标识一个唯一的网络连接。所以在通信过程中,套接字是成对出现的。 - 一个文件描述符指向一个
socket
套接字(内部由内核借助两个发送/接受缓冲区实现)。下图对比了网络套接字和Unix Domain Socket本地套接字两种类型,前者通过 IP+Port 即利用网络协议栈发送数据,而后者利用系统创建的 .sock 文件进行通信,故两者应用场景也不同;前者常用于不同主机进程间通信,后者则适用于本地不同进程间的无损、高速通信。
预备知识:
网络数据流的顺序:先发出低地址的数据,后发出高地址的数据。
网络字节序:
- 大端字节序(网络字节序、PowerPC架构):高位存低地址,低位存高地址。
- 小端字节序(Intel x86架构):高地址存高位,低地址存底低位。
#include <arpa/inet.h>
// Convert values between host and network byte order.
uint32_t htonl(uint32_t hostlong); --> IP
uint32_t htons(uint32_t hostshort); --> Port
uint32_t ntohl(uint32_t hostlong); --> IP
uint32_t ntohs(uint32_t hostshort); --> Port
IP转换函数:
#include <arpa/inet.h>
/* 本地字节序(const char* IP) --> 网络字节序 */
// 参数:
// af:AF_INET(IPv4)、AF_INET6(IPv6)
// src:传入 IP 地址(点分十进制形式)
// dst:传出转换后的网络字节序的 IP 地址
int inet_pton(int af, const char* src, void* dst);
// 返回值:成功则返回1,异常则返回0即src指向的不是一个有效的IP地址,失败则返回-1
/* 网络字节序 --> 本地字节序(const char* IP) */
// af:AF_INET、AF_INET6
// src:网络字节序的 IP 地址
// dst:本地字节序(点分十进制形式的 IP 地址)
// size:dst 的大小
const char* inet_ntop(int af, const void* src, char* dst, socklen_t size);
sockaddr&网络套接字&本地套接字地址结构:
现在的sockaddr
退化成了void*
的作用,传递一个地址给函数,其会根据地址族确定到底调用按个函数sockaddr_in/sockaddr_in6
,函数内部会强制将类型转化为所需的地址类型。
struct sockaddr
{
sa_family_t sin_family; /* address family : AF_INET、AF_INET6、AF_UNIX */
char sa_data[14]; /* 14 bytes, 包括套接字中的目标地址和端口信息 */
};
// 注意:
// 1. sockaddr是给操作系统用的
// 2. 程序员应该使用sockaddr_in来表示地址,(sockaddr_in区分了地址和端口,使用更方便)
// 3. 使用系统调用时,用“类型、IP地址、端口”填充sockaddr_in结构体,之后需要强制转换成sockaddr,才能作为参数传递给系统调用函数;sockaddr_un也是如此。
struct sockaddr_in
{
sa_family_t sin_family; /* address family : AF_INET、AF_INET6、AF_UNIX */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
char sin_zero[8]; /* 8 bytes zero this if you want to */
// sin_zero用来填充字节,使sockaddr_in、sockaddr保持一样大小
};
/*
// 支持IPv4和IPv6
// inet_pton()函数,可转换如下:IPv4 --> in_addr、IPv6 --> in6_addr
struct in_addr
{
uint32_t s_addr; // Internet address : network byte order
};
struct in6_addr
{
uint64_t l_addr; // Internet address : network byte order
};
*/
// 头文件: sys/un.h
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; // 地址族协议 af_local
char sun_path[UNIX_PATH_MAX]; // 套接字文件的《绝对路径》, 这是一个“伪文件”, 大小永远=0
};
// 套接字文件无需我们手动创建,服务端在绑定的时候会自动创建。
- 网络套接字的socket地址:IP地址 + 端口号
- UNIX Domain Socket的地址:socket类型的文件(由bind调用创建,若调用bind时该文件已存在则会返回错误)在文件系统中的路径
注意:所有专用socket
地址结构体类型的变量,都需要强制转换成通用socket
地址类型sockaddr
(所有的socket编程接口使用的地址参数类型都是sockaddr
)。
// 网络套接字,使用举例:
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6;
addr.sin_port = htons(端口号);
// 方式一:
inet_pton(AF_INET, string类型的IP, &(addr.sin_addr.s_addr));
// 方式二:
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 取出系统中,有效的任意IP地址。
// 本地套接字,使用举例:
static const boost::filesystem::path s_sock_path = boost::filesystem::path(env.data_path()) / boost::filesystem::path("run") / boost::filesystem::path("log_collector.sock");
_server.sun_family = AF_UNIX;
strncpy(_server.sun_path, s_sock_path.c_str(), sizeof(_server.sun_path) - 1);
setsockopt函数:
int setsockopt(int sockfd, int level, int optname, const void* optval, socklen_t optlen)
// 函数功能:用来设置参数sockfd所指定的socket的状态。
// - level代表欲设置的网络层,SOL_SOCKET(表示socket层);
// - optname代表欲设置的选项参数,SO_REUSEADDR(地址复用)、SO_SNDBUF(发送缓冲区);
// - optval代表欲设置的值,1/0(启用/不启用地址复用)、BufferSize(缓冲区的大小);
// - optlen代表optval的长度。
socket文件描述符号:
使用cat
命令查看一个进程可以打开的socket
描述符的上限。
cat /proc/sys/fs/file-max
一个进程允许使用的文件描述符的个数:ulimit - a
查看。
如果有需要,可通过修改配置文件的方式,修改该值。
sudo vi /etc/security/limits.conf
TCP相关的socket函数
创建socket函数:
#include <sys/socket.h>
// domain:底层协议族:AF_INET、AF_INET6、AF_UNIX
// type:哪个服务:SOCK_STREAM(TCP字节流)、SOCK_DGRAM(UDP数据报)
// protocol:默认为0,即根据type选择指定协议(故有前两个参数足矣)
int fd = socket(int domain, int type, int protocol);
// 返回值:成功则返回一个socket文件句柄fd(或称文件描述符),失败则返回-1并设置errno
bind绑定(命名)socket:
#include <sys/socket.h>
// sockfd:socket文件描述符,即socket()函数的返回值
// addr:将addr所指的socket地址分配给 “未命名的sockfd”,即将 “IP + port” 与 sockfd 绑定
// addrlen:该socket地址结构的大小 sizeof(addr);
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen); // 给socket绑定一个地址结构 “IP + Port”
// 返回值:成功则返回0,失败则返回-1并设置errno。
listen监听socket:
socket
被绑定之后,还不可以马上接受客户连接,需要创建一个监听队列存放待处理的客户连接,即设置监听的上限。
#include <sys/socket.h>
/* 设置同时与服务器建立连接的上限数(同时进行3次握手的客户端数量),并不阻塞于此 */
// sockfd:被监听的socket,本质是socket()函数的返回值
// backlog:提示内核监听队列的最大长度,即同时与服务器建立连接的上限数(最大为128)
int listen(int sockfd, int backlog);
// 返回值:成功则返回0,失败则返回-1并设置errno
accept接受连接:
阻塞等待直到有客户端尝试与该服务器监听的端口连接,则从listen
监听队列中取出连接,最终返回能与客户端通信的文件描述符。
#include <sys/socket.h>
// sockfd:socket函数返回值
// 传出参数addr:成功与服务器建立连接的那个 “客户端的地址结构”
// 传入传出参数addrlen:传入的是addr的大小、传出的是实际的客户端addr的大小
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen); // 阻塞直到有客户端连接
// 返回值:
// 1. 成功则返回一个 “服务器能与客户端进行通信的socket对应的文件描述符”
// 2. 失败则返回-1并设置errno
connect发起连接:
服务器是通过listen
调用来被动接受连接,客户端则是通过connect
主动与服务器建立连接。
#include <sys/socket.h>
// sockfd:系统调用socket()返回的一个fd文件句柄(文件描述符)
// addr:服务器监听的socket地址
// addrlen:服务器监听的socket地址的长度
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
// 返回值:
// 1. 成功则返回0,之后“客户端就可以通过读写sockfd来与服务器进行通信”
// 2. 失败返回-1并设置errno
UDP相关的socket函数
recvfrom函数:
#include <sys/types.h>
#include <sys/socket.h>
// 读取sockfd上的数据
// buf、len参数:分别指定缓冲区的位置和大小
// 传出参数src_addr:每次读取数据都需要获取发送端的socket地址
// 传入传出参数addrlen:指定地址长度
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
// 返回值:成功则返回接收到的字节数,失败则返回-1并置errno,0则表示对端关闭
sendto函数:
#include <sys/types.h>
#include <sys/socket.h>
// 往sockfd上写入数据
// buf和len参数:分别为存储数据的缓冲区的位置和大小
// dest_addr指定接收方的socket地址,addrlen指定地址长度
size_t sendto(int sockfd, void* buf, size_t len, int flags, struct sockaddr* dest_addr, socklen_t addrlen);
// 返回值:成功则返回写出的字节数,失败则返回-1并置errno
close和shutdown函数对比:
// close()只关闭指向该缓冲区(用来socket通信的)的一个的文件描述符;shutdown()关闭所有指向该缓冲区的文件描述符
shutdown(int fd, int how);
// how : SHUT_RD 关读端(读缓冲区)
// SHUT_WR 关写端(写缓冲区)
// SHUT_RDWR 关读写端(读/写缓冲区)
本地套接字(UNIX Domain Socket):
本地套接字(Unix域套接字)是用于在同一台机器上的不同进程之间进行高速、低延迟通信的一种方式,如前后台进程之间的通信。它不涉及网络协议,而是直接在文件系统中创建一个unix套接字文件,进程可以通过访问该文件进行通信。
Socket API原本是为网络通讯设计的,但后来在Socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback回环地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。
深入剖析进程间通信:Unix 套接字、共享内存与IP协议栈的性能比较:
- 易用性:消息队列 > unix域套接字 > 管道 > 共享内存(常常需要搭配信号量)
- 效率:共享内存 > unix域套接字 > 管道 > 消息队列
UNIX Domain Socket是全双工的,提供面向流(SOCK_STREAM)和面向数据包(SOCK_DGRAM)两种API接口,类似于TCP(提供面向连接的服务,保证数据的有序无损传输)和UDP(是无连接的,不保证数据顺序和可靠性,但更适合小批量、无需顺序保证的数据交换),但均是可靠的,消息既不会丢失也不会顺序错乱。
TCP实现本地套接字:
server.c : 本地socket通信服务端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/un.h>
#define SOCK_PATH_SERV "./server.sock"
int main()
{
// 创建socket
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(sfd < 0) {
perror("socket error");
return -1;
}
// 删除socket文件,避免bind失败
unlink(SOCK_PATH_SERV);
// 绑定bind
struct sockaddr_un serv;
memset(&serv, 0, sizeof(serv));
serv.sun_family = AF_UNIX;
strcpy(serv.sun_path, SOCK_PATH_SERV);
int ret = bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
if(ret < 0) {
perror("bind error");
return -1;
}
// 监听listen
listen(sfd, 10);
// 接收新的连接-accept
struct sockaddr_un client;
memset(&client, 0, sizeof(client));
socklen_t len = sizeof(client);
int cfd = accept(lfd, (struct sockaddr *)&client, &len);
if(cfd < 0) {
perror("accept error");
return -1;
}
printf("client->[%s]\n", client.sun_path);
int n;
char buf[1024];
while(1) {
// 读数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n <= 0) {
printf("read error or client close, n==[%d]\n", n);
break;
}
printf("n==[%d], buf==[%s]\n", n, buf);
// 发送数据
write(cfd, buf, n);
}
close(sfd);
close(cfd);
return 0;
}
client.c : 本地socket通信客户端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/un.h>
#define SOCK_PATH_SERV "./server.sock"
#define SOCK_PATH_CLI "./client.sock"
int main()
{
// 创建socket
int cfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(cfd < 0) {
perror("socket error");
return -1;
}
// 删除socket文件,避免bind失败
unlink(SOCK_PATH_CLI);
// 绑定bind
struct sockaddr_un client;
memset(&client, 0, sizeof(client));
client.sun_family = AF_UNIX;
strcpy(client.sun_path, SOCK_PATH_CLI);
int ret = bind(cfd, (struct sockaddr *)&client, sizeof(client));
if(ret < 0) {
perror("bind error");
return -1;
}
struct sockaddr_un serv;
memset(&serv, 0, sizeof(serv));
serv.sun_family = AF_UNIX;
strcpy(serv.sun_path, SOCK_PATH_SERV);
ret = connect(cfd, (struct sockaddr *)&serv, sizeof(serv));
if(ret < 0) {
perror("connect error");
return -1;
}
int n;
char buf[1024];
while(1)
{
memset(buf, 0x00, sizeof(buf));
n = read(STDIN_FILENO, buf, sizeof(buf));
buf[strlen(buf) - 1] = '\0'; // 将换行符替换为字符串结束符
// 发送数据
write(cfd, buf, n - 1);
// 读数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n <= 0)
{
printf("read error or client close, n==[%d]\n", n);
break;
}
printf("n==[%d], buf==[%s]\n", n, buf);
}
close(cfd);
return 0;
}
UDP实现本地套接字:
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/un.h>
#define SOCKET_PATH_SERV "./sock_server.sock"
int main(int argc, const char *argv[])
{
//1、创建用于通信的服务器套接字文件描述符
int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if(sfd == -1) {
perror("Define socket error!");
return -1;
}
// 删除socket文件,避免bind失败
unlink(SOCKET_PATH_SERV);
// bind绑定sfd和SOCKET_PATH_SERV
struct sockaddr_un server_addr;
server_addr.sun_family = AF_UNIX; //通信域
strncpy(server_addr.sun_path, SOCKET_PATH_SERV, sizeof(server_addr.sun_path) - 1);
if(bind(sfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind socket error!");
return -1;
}
char buf[128] = "";
struct sockaddr_un client_addr; // 接受对端地址信息
socklen_t addrlen = sizeof(client_addr); // 接受地址长度
while(1) {
// 清空容器
memset(&buf, 0, sizeof(buf));
// 从套接字中读取数据并获取客户端的sockaddr_un即client_addr
int n = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, &addrlen);
if (n == 0) {
printf("client's socket %s have closed!\n", client_addr.sun_path);
continue;
}
printf("n==[%d], buf==[%s]\n", n, buf);
// 将收到的消息进行处理后,返回数据(这里直接返回原发送数据)
if(sendto(sfd, buf, strlen(buf), 0, (struct sockaddr*)&client_addr, sizeof(client_addr)) == -1) {
perror("Sent data to client by socket failure!");
break;
}
}
close(sfd);
return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/un.h>
#define SOCKET_PATH_CLI "./sock_client.sock"
#define SOCKET_PATH_SERV "./sock_server.sock"
int main()
{
// 创建socket
int cfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if(cfd < 0) {
perror("Define socket error");
return -1;
}
// 删除socket文件,避免bind失败
unlink(SOCKET_PATH_CLI);
// bind绑定cfd和SOCKET_PATH_CLI
struct sockaddr_un client_addr;
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sun_family = AF_UNIX;
strncpy(client_addr.sun_path, SOCKET_PATH_CLI, sizeof(client_addr.sun_path) - 1);
int ret = bind(cfd, (struct sockaddr *)&client_addr, sizeof(client_addr));
if(ret < 0) {
perror("Bind socket error!");
return -1;
}
// 连接server端的SOCKET_PATH_SERV
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path, SOCKET_PATH_SERV);
ret = connect(cfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if(ret < 0) {
perror("Connect server's socket error!");
return -1;
} else {
printf("Connect server's socket %s success!\n", SOCKET_PATH_SERV);
}
int n;
char buf[1024];
while(1)
{
memset(buf, 0x00, sizeof(buf));
n = read(STDIN_FILENO, buf, sizeof(buf));
buf[strlen(buf) - 1] = '\0';
//发送数据
write(cfd, buf, n - 1);
// 此时,buf中只有'\n'
if (n == 1) {
printf("Client terminate to send data by socket %s", SOCKET_PATH_CLI);
break;
}
//读数据
memset(buf, 0x00, sizeof(buf));
n = read(cfd, buf, sizeof(buf));
if(n <= 0)
{
printf("read error or client close, n==[%d]\n", n);
break;
}
printf("n==[%d], buf==[%s]\n", n, buf);
}
close(cfd);
return 0;
}
基于UDP实现server对client发送的数据进行直接缓存:
常用于同一主机上,一个进程client用来收集整理埋点/日志数据,另一个进程server用来将其同步上传到云端;两个进程之间通过Unix套接字进行IPC通信,不依赖网络协议栈故可实现无损、高效的传输。
注意⚠️:此种场景下,客户端不需要得到服务端的回应,故可以不需要在客户端配置.sock
文件,而选择将其文件描述符cfd
直接设置为O_NONBLOCKING
即可,有数据立即传输到服务端。如果要考虑效率的话,可以通过增加逻辑,当数据积累达到一定上限后,再进行传输。
server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/un.h>
#define SOCKET_PATH_SERV "./sock_server.sock"
#define SAVE_TXT "./client_data.txt"
int main(int argc, const char *argv[])
{
//1、创建用于通信的服务器套接字文件描述符
int sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if(sfd == -1) {
perror("Define socket error!");
return -1;
}
// 删除socket文件,避免bind失败
unlink(SOCKET_PATH_SERV);
// 绑定bind
struct sockaddr_un server_addr;
server_addr.sun_family = AF_UNIX; //通信域
strncpy(server_addr.sun_path, SOCKET_PATH_SERV, sizeof(server_addr.sun_path) - 1);
if(bind(sfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind socket error!");
return -1;
}
char buf[128] = "";
struct sockaddr_un client_addr; // 接受对端地址信息
socklen_t addrlen = sizeof(client_addr); // 接受地址长度
FILE *fp; // 声明一个FILE类型的指针
// 使用fopen函数以写入模式("w")打开文件。如果文件不存在,将创建文件。
// 注意:确保你有权限在当前目录下创建文件。
fp = fopen(SAVE_TXT, "w");
if (fp == NULL) {
perror("Error opening file");
return -1; // 如果文件打开失败,打印错误信息并退出
}
while(1) {
// 清空容器
memset(&buf, 0, sizeof(buf));
// 从套接字中读取数据
int n = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&client_addr, &addrlen);
if (n == 0) {
printf("client's socket %s have closed!\n", client_addr.sun_path);
continue;
}
printf("n==[%d], buf==[%s]\n", n, buf);
// 将收到的消息进行处理后,直接存储数据,并不会给客户端返回任何值
// 使用fputs函数将字符串写入文件
fputs(buf, fp);
// 确保数据被立即写入文件
fflush(fp);
}
// 关闭文件
fclose(fp);
close(sfd);
return 0;
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/un.h>
#include <sys/fcntl.h>
#define SOCKET_PATH_SERV "./sock_server.sock"
const int BUFFER_SIZE = 1024;
int main()
{
// 创建socket
int cfd = socket(AF_UNIX, SOCK_DGRAM, 0);
if(cfd < 0) {
perror("Define socket error");
return -1;
}
struct sockaddr_un server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path, SOCKET_PATH_SERV);
if (setsockopt(cfd, SOL_SOCKET, SO_SNDBUF, &BUFFER_SIZE, sizeof(BUFFER_SIZE)) == -1) {
perror("set socket buffer size failure!");
return -1;
}
// set non blocking
int flags = fcntl(cfd, F_GETFL, 0);
fcntl(cfd, F_SETFL, flags | O_NONBLOCK);
flags = fcntl(cfd, F_GETFL, 0);
if ((flags & O_NONBLOCK) == 0) {
perror("cfd set non blocking failure!");
return -1;
}
if(connect(cfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("Connect server's socket error!");
return -1;
} else {
printf("Connect server's socket %s success!\n", SOCKET_PATH_SERV);
}
int n;
char buf[1024];
while(1)
{
memset(buf, 0x00, sizeof(buf));
n = read(STDIN_FILENO, buf, sizeof(buf));
buf[strlen(buf) - 1] = '\0';
//发送数据
write(cfd, buf, n - 1);
// 此时,buf中只有'\n'
if (n == 1) {
printf("Client terminate to send data by socket %s", SOCKET_PATH_CLI);
break;
}
}
close(cfd);
return 0;
}
网络套接字函数:
基于TCP的socket模型:
基于UDP的socket模型:
C/S模型-TCP实现:
TCP三次握手、状态变换、函数调用返回、队列状态综合图:
客户端:socket()
--> connect()
阻塞在这里、SYN_SENT
状态 --> RTT
--> connect()
返回、ESTABLISHED
状态
服务端:socket()、bind()、listen()
--> accept()
阻塞在这里、LISTEN
状态 --> 在未完成连接队列中建立条目、SYN_RCVD
状态 --> RTT
--> ESTABLISHED
状态、该条目从未完成队列中移到已完成队列中且accept
返回
TCP状态转换图:
TCP通信流程:
注意:
- 主动关闭的一方可能是
client/server
,且主动关闭的一方均要等通过TIME_WAIT
等待2MSL
的时长。该关闭方式会将发送缓冲区的数据全部发送完后,才会正常的关闭TCP连接。 - 如果发送的是
RST
数据包,则是不正常的关闭,即不会经历四次挥手,故主动关闭连接的一方也不会进入TIME_WAIT
状态。
2MSL:
等待2MSL
的意义?
保证最后一个ACK
能成功被对端接收(等待期间,如果对端没有收到我发的ACK
,对端可以再发送FIN
请求)。
端口复用:
如果server
先关闭进入TIME_WAIT
状态(client
后关闭),之后server
立即重启,但会提示“端口已被占用”,因为此时server
是主动发起关闭的一端,会经历从TIME_WAIT
状态开始的2MSL
的等待时间。
j解决办法:使用setsockopt()
设置sock
描述符的选项为SO_REUSEADDR = 1
,表示允许创建端口号相同但IP
地址不同的多个sock
描述符。
主要解决的问题:TIME_WAIT
状态,导致bind
失败的问题。
// 在server.c中,socket()和bind()之间,插入如下代码:(解决了server关闭后,可立即重启的问题)
int opt = 1;
setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));
// 返回值:成功则返回0,失败则返回-1并置errno
TCP连接和释放的过程:
- 三次握手:
三次握手的必要性?
在两次握手的情况下,服务端没有中间状态来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
三次握手后,accept()
会返回从全连接队列队首取出的套接字,即通过该套接字可以实现server与client之间的通信。
思考题:
- 如果两半连接和全连接队列的数据量之和超过了
backlog
,此时再有客户端发送SYN
请求,服务器会作何反应?服务器会忽略该SYN
,客户端则因没有收到SYN+ACK
数据包而超时重传SYN
包。 - “半连接队列 --> 全连接队列 -->
accept()
函数返回”存在时间差的问题,如果该是时间差内客户端发送了数据,则会存在服务端该套接字对应的接收缓冲区(该缓冲区的大小,决定了可接受的数据大小)。 backlog
进一步明确的规定:指定给定套接字上内核为之排队的最大已完成连接数,即全连接队列中的条目数。一般建议backlog=300
左右。
- 主动发起连接请求端:发送
SYN
标志位,请求建立连接。携带序列号seq
、数据字节数(0)、滑动窗口大小。 - 被动接受连接请求端:发送
ACK
标志位,同时携带SYN
请求标志位。携带序列号seq
、确认序号ack
、数据字节数(0)、滑动窗口大小。 - 主动发起连接请求端:发送
ACK
标志位,应答服务器连接请求,携带确认序号ack
。因为第三次握手,如果携带数据,则也需要携带序列号seq
、数据字节数(非0)、滑动窗口大小。
-
四次挥手:
-
半关闭过程:
主动关闭连接请求端,发送
FIN
标志位。被动关闭连接请求端,应答
ACK
标志位。 -
连接全部关闭:
被动关闭连接请求端,发送
FIN
标志位。主动关闭连接请求端,应答
ACK
标志位。
-
简单的通信过程实现:
server.c:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h> // toupper()/tolower()
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_PORT 8000
void sys_error(const char* str)
{
perror(str);
exit(1);
}
int main()
{
// 创建socket
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0); // 使用IPv4且基于TCP进行通信
if (listen_sockfd == -1)
{
sys_error("socket error");
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 表示接收任意IP的连接请求
// 绑定服务器地址结构
int ret = bind(listen_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1)
{
sys_error("bind error");
}
// 设置同时监听上限
ret = listen(listen_sockfd, 128);
if (ret == -1)
{
sys_error("listen error");
}
// 阻塞监听客户端连接,获取客户端的sockaddr_in的socket结构,并返回用于与客户端通信的fd文件描述符
struct sockaddr_in client_addr; socklen_t client_addrlen;
int client_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &client_addrlen);
if (client_sockfd == -1)
{
sys_error("accept error");
}
// 获取客户端的地址
char client_IP[1024];
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)); // 返回值与client_IP一致
printf("%s : %d\n", client_IP, ntohs(client_addr.sin_port));
while (1)
{
char buf[1024];
// 阻塞等待获取客户端数据,并返回实际读到的字节数
ret = read(client_sockfd, buf, sizeof(buf));
if (ret == 0) // !!!重点:已经读到结尾(对端已关闭)
{
break;
}
write(STDOUT_FILENO, buf, ret);
// 逻辑处理:将小写转大写
for (int i = 0; i < ret; ++i)
{
buf[i] = toupper(buf[i]);
}
write(client_sockfd, buf, ret);
}
close(listen_sockfd);
close(client_sockfd);
return 0;
}
client.c:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_PORT 8000
void sys_error(const char* str)
{
perror(str);
exit(1);
}
int main()
{
// 创建socket
int client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (client_sockfd == -1)
{
sys_error("socket error");
}
// 将服务器监听的IP地址和端口号port,以及使用的IP地址族,写入sockaddr_in结构体,组成服务器的socket地址
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, "127.0.0.1", &(server_addr.sin_addr.s_addr));
// 与服务器建立连接
int ret = connect(client_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1)
{
sys_error("connect error");
}
char buf[1024];
while (1)
{
// 阻塞等待客户端输入数据,并将数据写入buf,返回实际写入buf的字节数ret
ret = read(STDIN_FILENO, buf, sizeof(buf));
if (ret == 0) // !!!重点:已经读到结尾(对端已关闭)
{
break;
}
write(client_sockfd, buf, ret);
// 阻塞等待服务端的返回数据,并将数据写入buf,返回实际写入buf的字节数ret
ret = read(client_sockfd, buf, sizeof(buf));
write(STDIN_FILENO, buf, ret);
}
close(client_sockfd);
}
C/S模型-UDP实现:
由于不需要建立连接,故无状态(通过netstat -apn
观察不到该IP:Port
的状态)、默认支持并发,即多路I/O。
简单的通信过程实现:
server.c:
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_PORT 8000
#define BUFSIZE 1024
void sys_error(const char* str)
{
perror(str);
exit(1);
}
int main()
{
int listen_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (listen_sockfd < 0)
{
sys_error("socket error");
}
// 初始化服务器的socket地址结构
struct sockaddr_in server_sockaddr;
memset(&server_sockaddr, 0, sizeof(struct sockaddr_in));
server_sockaddr.sin_family = AF_INET; // 协议族
server_sockaddr.sin_port = htons(SERVER_PORT);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意本地地址
int ret = bind(listen_sockfd, (struct sockaddr*)&server_sockaddr, sizeof(struct sockaddr_in));
if(ret < 0)
{
sys_error("bind error");
}
struct sockaddr_in client_sockaddr; int client_sockaddr_len = sizeof(struct sockaddr_in);
char buf[BUFSIZE];
printf("accepting connection ... \n");
while(1)
{
ret = recvfrom(listen_sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&client_sockaddr, &client_sockaddr_len);
if (ret == -1)
{
sys_error("recvfrom error");
}
// 获取发送数据的客户端的IP地址和port端口号:
char client_ip_addr[1024];
inet_ntop(AF_INET, &client_sockaddr.sin_addr, client_ip_addr, sizeof(client_ip_addr));
printf("data from %s : %d\n", client_ip_addr, ntohs(client_sockaddr.sin_port));
// 服务器执行逻辑,如小写转大写
for (int i = 0; i < ret; ++i)
{
buf[i] = toupper(buf[i]);
}
ret = sendto(listen_sockfd, buf, ret, 0, (struct sockaddr*)&client_sockaddr, client_sockaddr_len);
if(ret == -1)
{
sys_error("sendto error");
}
// 将数据发送到客户端的IP地址和port端口号:
printf("data sent to %s : %d\n", client_ip_addr, ntohs(client_sockaddr.sin_port));
}
close(listen_sockfd);
return 0;
}
client.c:
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include <arpa/inet.h>
#include<sys/socket.h>
#define SERVER_PORT 8000
#define BUFSIZE 1024
void sys_error(const char* str)
{
perror(str);
exit(1);
}
int main()
{
int client_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 初始化服务器的socket地址结构
struct sockaddr_in server_sockaddr;
memset(&server_sockaddr, 0, sizeof(struct sockaddr_in));
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, "127.0.0.1", &(server_sockaddr.sin_addr.s_addr));
char buf[BUFSIZE]; int ret;
while (fgets(buf, BUFSIZE, stdin) != NULL)
{
int server_sockaddr_len = sizeof(struct sockaddr_in);
ret = sendto(client_sockfd, buf, strlen(buf), 0, (struct sockaddr*)&server_sockaddr, server_sockaddr_len);
if (ret == -1)
{
sys_error("sendto error");
}
ret = recvfrom(client_sockfd, buf, sizeof(buf), 0, NULL, 0); // NULL和0表示不关心对端的socket地址信息
if (ret == -1)
{
sys_error("recv_from");
}
write(STDOUT_FILENO, buf, ret);
}
close(client_sockfd);
return 0;
}
Linux
下的网络IO模型:
典型的一次IO的两个阶段是什么?
- 数据准备:根据系统IO操作的就绪状态,分为阻塞/非阻塞。
- 阻塞:调用 IO 方法的线程进入阻塞状态;
- 非阻塞:不会改变线程的状态,而是通过函数返回值判断,如:
read
系统调用:- 返回值为-1且
errno=EAGAIN
,则表示因没有数据而返回; - 返回值为0,则表示对端已关闭;
- 返回值>0,则表示读到了返回值大小的数据;
- 返回值为-1且
- 数据读写:根据应用程序和内核的交互方式,分为同步/异步。
陈硕大神原话:
在处理 IO 的时候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是异步IO。Linux通过AIO
可使用异步 IO 方式。
同步/异步:
- 同步:A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),数据的读写都是由请求方A自己来完成的(不管是阻塞还是非阻塞);
- 异步:A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就可以处理其它逻辑了,当B监听到事件发生(内核帮我们做)后,会用事先约定好的通知方式,通知A处理结果。
注意:上面提到的同步/异步,指的是针对系统IO而言的,当然具体的业务中可会涉及到同步/异步。
同步/异步、阻塞/非阻塞的四种组合方式:
采用浅显易懂的方式说明:
五种IO
模型:
阻塞blocking
:BIO
该模型存在的问题:
- 服务端为单线程(或称单进程):客户端与服务端建立了连接,但连接的客户端迟迟不发数据,进程就会一直堵塞在read()方法上,导致其他客户端也不能进行连接,即无法满足高并发的要求。
- 服务端为多线程(单进程中的多线程,需要一个容器记录 “connfd与线程” 的对应关系):只要连接了一个socket,操作系统分配一个线程来处理,这样read()方法堵塞在每个具体线程上而不堵塞主线程(即该单进程),从而实现多个socket的操作。
问题出现的原因:操作系统中用户态无法直接开辟线程,需要调用内核来创建的一个线程,这其中还涉及到用户状态的上下文切换。
存在的问题:如果每来一个客户端就开辟一个线程,需要不断地在用户态和内核态之间切换来创建线程,这样十分耗资源。
可能的解决方案:
1)引入线程池,但无法确定初始的线程数量;
2)引出NIO
非阻塞IO模型。
非阻塞non-blocking
:NIO
NIO
的特点:用户进程需要不断的主动询问内核数据是否准备好了,即一句话用轮询替代阻塞!!!
NIO模式
中,一切都是非阻塞的:
accept()
是非阻塞的,没有客户端连接时,就返回无连接标识。read()
是非阻塞的,读取不到数据就返回空闲中标识,读取到数据时只阻塞read()方法读数据的时间。
NIO模式
中,只有一个线程:当客户端与服务端进行连接,该socket就会加入到一个数组中,并隔一段时间遍历一次该数组,检查socket的read()方法能否读到数据,即使用一个线程来处理多个客户端的连接和读取。
NIO模式
的优缺点:
- 优点:不会阻塞在内核的等待数据过程,每次发起的 I/O 请求可以立即返回,实时性较好。
- 缺点:用户态的轮询将会不断地询问内核(用户态判断socket是否有数据还是调用内核的read()方法实现的),涉及大量的用户态和内核态的切换,这会占用大量的 CPU 时间,系统资源利用率较低且很多都是无用功(有的socket可能很长时间处于无数据传输的状态,但是NIO模型还需要不断地通过用户态和内核态的切换来判断其是否有数据),故 Web 服务器不使用该 I/O 模型。
结论:通过将一批文件描述符通过一次系统调用传给Linux内核,由内核层去遍历,即IO多路复用主要目的:将遍历发现哪些socket有读/写事件发生的工作,交给Linux内核,不再两态转换而是直接从内核获得发生读/写事件的socket,因为内核是非阻塞的。
IO
多路复用IO Multiplexing
:
I/O 多路复用的特点:
- 实现了一个进程能同时监控多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要 select 、 poll 、 epoll 来配合。
- 多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始处理业务。
过程:将用户socket对应的fd(socket应采用非阻塞模式)注册进epoll后,epoll帮你监听哪些socket上有消息到达,从而避免了大量的无用操作。整个过程只在调用select、poll、epoll时才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,即事件驱动,所谓的reactor反应模式。
多路复用快的原因:操作系统提供了这样的系统调用,使得原来的 while 循环里多次系统调用,变成了一次系统调用 + 内核层遍历这些文件描述符。
信号驱动signal-driven
:
内核的第一阶段是异步、第二阶段是同步;与非阻塞IO的区别在于:它提供了消息通知机制,不需要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。
异步asynchronous
:
struct aiocb {
int aio_fildes
off_t aio_offset
volatile void *aio_buf
size_t aio_nbytes
int aio_reqprio
struct sigevent aio_sigevent
int aio_lio_opcode
};
总结:
高并发服务器:
补充细节:
高并发服务器的通讯示意图:
read函数返回值:
> 0
,表示实际读到的字节数= 0
,已经读到结尾(对端(客户/服务端)已关闭)-1
,则需要进一步判断errno
的值:errno = EAGIN || EWOULDBLOCK
:设置了非阻塞方式,读时没有数据到达。errno == EINTR
,表示慢速系统调用read
被中断。errno == ECONNRESET
说明连接被重置,需要close
,移出监听队列。errno
为“其他情况,出现异常”。
多进程并发服务器:
使用细节:
- 父进程中,最大文件描述符的个数(父进程中,需要
close
关闭accept
返回的新的文件描述符)。 - 系统内,创建进程个数(与内存大小有关)。
- 进程创建过多,是否会降低整个服务器的性能(进程调度)。
- 需要在父进程中,通过注册的捕捉函数(终止掉所有已经结束的子进程,防止僵尸进程出现)捕捉子进程发生状态变化的**
SIGCHILD
信号**,同时还会继续阻塞监听客户端的连接…。
server.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERVER_PORT 8000
void sys_error(const char* str)
{
perror(str);
exit(1);
}
void tfunc(int signo)
{
if (signo == SIGCHILD)
{
// 非阻塞形式,回收所有给父进程发送SIGCHILD信号的子进程
while (waitpid(0, NULL, WNOHANG) > 0);
}
}
int main(int argc, char* argv[])
{
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sockfd == -1)
{
sys_error("socket error");
}
struct sockaddr_in server_addr;
// memset(&addr, 0, sizeof(addr));
bzero(&server_addr, sizeof(server_addr)); // 将addr结构体清零
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
int ret = bind(listen_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1)
{
sys_error("bind error");
}
ret = listen(listen_sockfd, 128);
if (ret == -1)
{
sys_error("listen error");
}
while (1)
{
struct sockaddr_in client_addr; socklen_t client_addr_len;
int client_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &client_addr_len);
// 获取客户端的地址
char client_IP[1024];
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_IP, sizeof(client_IP));
printf("%s : %d\n", client_IP, ntohs(client_addr.sin_port));
pid_t pid = fork(); // fork的每个子进程处理一个客户端的连接
if (pid < 0) {
sys_error("fork error");
} else if (pid == 0) { // 子进程,用来处理某一个客户端连接
close(listen_sockfd);
while (1)
{
char buf[1024];
ret = read(client_sockfd, buf, sizeof(buf));
if (ret == 0) // !!!重点:已经读到结尾(对端已关闭)
{
break;
}
write(STDOUT_FILENO, buf, ret);
// 逻辑处理:将小写转大写
for (int i = 0; i < ret; ++i)
{
buf[i] = toupper(buf[i]);
}
write(client_sockfd, buf, ret);
}
} else { // 父进程,等待其他客户端连接,并回收已经处理结束的子进程(僵尸进程)
// 当子进程状态变化时(结束、终止等),会发送SIGCHILD信号给父进程
// 父进程调用注册的捕捉函数,捕捉该SIGCHILD信号
struct sigaction act;
act.sa_handler = sig_catch;
sigemptyset(&act.sa_mask);
// flags=0,即在捕捉函数执行期间,屏蔽子进程的SIGCHILD信号,使用waitpid(0, NULL, WNOHANG)回收所有结束的子进程
act.sa_flags = 0;
ret = sigaction(SIGCHILD, &act, NULL);
if (ret == -1)
{
sys_error("sigaction error");
}
close(client_sockfd);
continue;
}
}
return 0;
}
多线程并发服务器:
使用细节:
- 注意一次最多能连接的客户端的数,本例中为256个
- 注意需要在主线程中,设置子线程为“分离状态”,即可自行回收线程资源。
server.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERVER_PORT 8000
struct sock_info {
struct sockaddr_in connect_addr;
int connect_fd;
};
void sys_error(const char* str)
{
perror(str);
exit(1);
}
void* tfunc(void* arg)
{
// 打印已连接的客户端的"Port : IP"
struct sock_info* ts = (struct sock_info*)arg;
char client_addr[1024];
inet_ntop(AF_INET, &(*ts).connect_addr.sin_addr, client_addr, sizeof(client_addr));
printf("%s : %d\n", client_addr, ntohs((*ts).connect_addr.sin_port));
while (1)
{
char buf[1024];
int ret = read(ts->connect_fd, buf, sizeof(buf));
if (ret == 0) // !!!重点:已经读到结尾(对端已关闭)
{
printf("....... the client %d closed ........\n", ts->connect_fd);
break;
}
write(STDOUT_FILENO, buf, ret);
// 逻辑处理:将小写转大写
for (int i = 0; i < ret; ++i)
{
buf[i] = toupper(buf[i]);
}
write(ts->connect_fd, buf, ret);
}
close(ts->connect_fd);
}
int main()
{
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
memset(&server_addr, sizeof(server_addr), 0);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(SERVER_PORT);
int ret = bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1)
{
sys_error("bind error");
}
ret = listen(listen_fd, 128); // 设置同一时刻,连接服务器的上限数
if (ret == -1)
{
sys_error("listen error");
}
struct sock_info ts[256]; // 只能允许创建的256个线程,来处理客户端的连接
int i = 0; // 0 <= i <= 255
while (1)
{
struct sockaddr_in client_addr;
socklen_t clientaddr_len;
// 阻塞监听客户端的连接请求
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &clientaddr_len);
ts[i].connect_addr = client_addr;
ts[i].connect_fd = client_fd;
printf("....... the client %d connected ........\n", client_fd);
pthread_t tid;
pthread_create(&tid, NULL, tfunc, (void*)&ts[i]); // 每个创建的线程都可用来接收和发送数据(客户端传来的 或 服务端处理过后传出的)
pthread_detach(tid); // 设置线程为分离状态,可独立回收资源,防止僵尸进程的出现
++i;
}
return 0;
}
多路I/O转接服务器:
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,空闲时会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时就会从阻塞态中唤醒,之后程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈。
select函数:
相关函数:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
/*
select 函数监视的文件描述符分3类:readfds、writefds和exceptfds,将用户传入的数组拷贝到内核空间调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写或者except)或超时(timeout指定等待时间),则函数立即返回。
select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
*/
// nfds:监听的所有文件描述符中,最大的文件描述符+1
// readfds:读,文件描述符监听集合(传入传出参数)
// writefds:写,文件描述符监听集合(传入传出参数)
// exceptfds:异常,文件描述符监听集合 (传入传出参数)
// timeout:
// >0,设置监听超时时长
// NULL,阻塞监听
// =0,非阻塞监听、轮询
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
// 返回值:>0,所有监听集合(readfds、writefds、exceptfds)中,满足对应事件的总数
// =0,没有满足监听条件的文件描述符
// -1,发生错误并置errno
void FD_ZERO(fd_set* set); // 将监听集合清零
void FD_CLR(int fd, fd_set* set); // 将一个文件描述符从监听集合中移除
void FD_SET(int fd, fd_set* set); // 将待监听的文件描述符,添加到监听集合中
int FD_ISSET(int fd, fd_set* set); // 判断一个文件描述符,是否在监听集合中
/* fd_set是一个bitmap,select系统调用发现socket有数据会将bitmap该位置置位,用户态只需遍历该fd_set即可获取哪个文件描述符有事件发生,避免了NIO中要不停通过轮询在两态之间切换来判断。 */
server.c:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#define SERVER_PORT 8000
void sys_error(const char* str)
{
perror(str);
exit(1);
}
int main()
{
int connect_sockfd;
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sockfd == 0)
{
sys_error("socket error");
}
int opt = 1;
setsockopt(listen_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(listen_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1)
{
sys_error("bind error");
}
ret = listen(listen_sockfd, 128);
if (ret == -1)
{
sys_error("listen error");
}
fd_set rset, allset; // 定义读集合rset、备份集合allset
FD_ZERO(&allset); // 清空备份集合
FD_SET(listen_sockfd, &allset); // 将待监听listen_sockfd添加到监听集合中
int max_sockfd = listen_sockfd;
while (1)
{
rset = allset; // 备份
// 使用select监听信号集rset,且每次循环都需要更新rset
int nReady = select(max_sockfd + 1, &rset, NULL, NULL, NULL);
if (nReady < 0)
{
sys_error("select error");
}
// 表示此时有客户端要与服务器进行连接
if (FD_ISSET(listen_sockfd, &rset) == 1) // listen满足监听的“读事件”
{
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 获取客户端的socket地址,并与客户端建立连接,得到连接后的文件描述符connect_sockfd
int connect_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &client_addr_len); // 建立连接,不会发生阻塞
FD_SET(connect_sockfd, &allset); // 将新产生的connect_fd,添加到监听集合中,监听数据“读事件”
if (max_sockfd < connect_sockfd)
{
max_sockfd = connect_sockfd;
}
if (nReady == 1) // 表示此时select只返回了一个,并且是listen_fd,后续无须执行
{
continue;
}
}
// 轮询所有的读文件描述符,是否有数据(优化:通过将所有待读文件描述符放入一个数组,来缩小轮询读文件描述符的范围)
for (int i = listen_sockfd + 1; i <= max_sockfd; ++i) // 处理满足读事件的文件描述符
{
if (FD_ISSET(i, &rset)) // 找到满足读事件的文件描述符
{
char buf[1024];
ret = read(i, buf, sizeof(buf));
if (ret == -1) {
sys_error("read error");
} else if (ret == 0) { // 监听到客户端已经关闭连接
close(i);
FD_CLR(i, &rset); // 将关闭的文件描述符,移除监听集合
} else if (ret > 0) { // 服务端处理客户端发来的数据,并将结果再发送回客户端
for (int j = 0; j < ret; ++j)
{
buf[j] = toupper(buf[j]);
}
write(i, buf, ret);
write(STDOUT_FILENO, buf, ret);
}
}
}
}
close(listen_sockfd);
return 0;
}
select与NIO的对比:
- select 本质把NIO中用户态要遍历的fd数组(每一个socket连接对应的fd)拷贝到了内核态,让内核态来遍历(因为用户态判断socket是否有数据还是要调用内核态的,所有拷贝到内核态后,遍历判断时就不用一直用户态和内核态频繁切换了)。
- select 系统调用后,返回了一个置位后的&rset,这样用户态只需进行很简单的二进制比较,就能很快知道哪些socket需要read数据,有效提高了效率。
select的优缺点:
-
优点:跨平台。
-
缺点:
- 监听上限,受文件描述符个数的限制,最大为1024个。
- 同一时刻通常只有其中几个或几百个连接在收发数据,其他连接可能处于只连接而不发送数据的状态。
select
和poll
函数则需要轮询所有建立连接的文件描述符,是否有读事件发生。
poll函数:
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
使用伪代码:
/* 模拟有5个客户端连接 */
for (int i = 0; i < 5; ++i)
{
memset(&client, 0, sizeof(client));
addlen = sizeof(client);
pollfds[i].fd = accept(sockfd, (struct sockaddr*)&client, &addrlen);
pollfds[i].events = POLLIN;
}
sleep(1);
while(1)
{
poll(pollfds, 5, 50000); // poll传入pollfds交给内核判断是否有事件发生,哪个fd发生事件其对应的revents置为1
for (int i = 0; i < 5; ++i)
{
if (pollfds[i].revents & POLLIN)
{
pollfds[i].revents = 0; // 找到后revents清零
memset(buffer, 0, MAXBUF);
read(pollfds[i].fd, buffer, MAXBUF);// 读取fd的数据到buffer中
puts(buffer);
}
}
}
poll的优缺点:
优点:
- poll使用
pollfd数组
来代替select中的bitmap
,数组没有1024的限制,可以一次管理更多的client。
与 select 的主要区别:去掉了 select 只能监听 1024 个文件描述符的限制。 - 当pollfds数组中有事件发生,相应的
revents
置位为1,遍历的时候又置位回零,实现了pollfd数组的重用。
缺点:
- pollfds数组拷贝到了内核态,仍然有开销。
- poll并没有通知用户态哪一个socket有数据,仍然需要
O(n)
的遍历。
epoll函数:
参考大佬写的NtyTcp
项目,学习epoll
三大函数的实现。
引入:
epoll是现在最先进的IO多路复用器(“多路”指的是多个网络连接,“复用”指的是复用同一个线程),是Linux
下多路复用I/O接口select/poll
的增强版本,Redis、Nginx都使用的是epoll。
- 一个socket的生命周期中只有一次从用户态拷贝到内核态的过程,开销小。
- 使用event事件通知机制,每次socket中有数据会主动通知内核,并加入到就绪链表中,不需要遍历所有的socket。
- 显著提升程序在大量并发连接中,只有少量活跃的情况下的系统
CPU
利用率。
1)复用文件描述符集合来传递结果,而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合。
2)获取事件时,无需遍历整个被侦听的描述符集,只要遍历那些被内核I/O
事件异步唤醒而加入Read
队列的描述符集合就行。
epoll是一种在Linux
上使用的I/O
多路复用并支持高并发的典型技术,不能跨平台。
epoll
除了提供select、poll
那种I/O
事件的电平触发LT
(Level Triggered
)外,还提供了边沿触发ET
(Edge Triggered
)。这使得用户空间程序有可能缓存I/O
状态,减少epoll_wait/epoll_pwait
的调用,提高应用程序效率。
常用的函数:
引言:红黑树是一种自平衡的二叉搜索树,用于在内核中高效地管理和操作数据。具有以下性质的二叉搜索树:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色的。
- 每个叶子节点(nullptr节点,即空节点)都是黑色的。
- 如果一个节点是红色的,则它的两个子节点都是黑色的。
- 对于每个节点,从该节点到其后代叶子节点的所有路径上,包含相同数量的黑色节点。
epoll_create函数:
#include <sys/epoll.h>
/* 创建一个监听红黑树 */
// size:创建的红黑树监听节点的数量,但仅供内核参考!!!
int epoll_create(int size);
// 返回值:成功则返回指向新创建的红黑树根节点的fd,失败则返回-1并置errno
epoll_ctl函数:
#include <sys/epoll.h>
/* 操作监听红黑树,来监听socket上的数据往来 */
// epfd:epoll_create()函数返回的监听树的句柄
// op:对该监听红黑树所作的操作(注意:该“事件对应的sockfd”,即是红黑树的“键”),有
// EPOLL_CTL_ADD(添加fd到监听红黑树)
// EPOLL_CTL_DEL(将一个fd从监听红黑树上摘下,即取消监听)
// EPOLL_CTL_MOD(修改fd在监听红黑树上的监听事件)
// fd:待监听的fd
/*
// 联合体:
typedef union epoll_data {
void *ptr; // 在epoll反应堆模型中,用来注册回调函数,内核会在条件满足时调用该回调函数
int fd; // 对应监听事件的fd
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; // epoll events => EPOLLIN(对应的文件描述符可以读,包括对端socket正常关闭造成的读事件)
// EPOLLOUT(对应的文件描述符可以写)
// EPOLLERR(对应的文件描述符发生错误)
// EPOLLET(边沿触发)
// EPOLLLT(水平触发,默认的触发方式)
epoll_data_t data; // user data variable
}
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
// 返回值:成功则返回0,失败则返回-1并置errno
epoll_wait函数:
#include <sys/epoll.h>
/* 阻塞监听,并获取内核的事件通知(本质是:从内核的双向链表中拿走有数据/有事件的sockfd【TCP链接】) */
// epfd:epoll_create()函数的返回值
// 传出参数events:用来存放内核得到的事件集合,可简单看作一个数组,元素的总个数为maxevents
// timeout设置超时时间:-1则阻塞、0立即返回、>0指定超时时间(毫秒)
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
// 返回值:>0则返回有多少个文件描述符有事件,=0则没有fd满足监听事件,出错则返回-1并置errno
epoll原理详解:
epoll_create详解:
当某一进程调用epoll_create
方法时,Linux内核会创建一个eventpoll
结构体,这个结构体中有两个成员与epoll
的使用方式密切相关。
struct eventpoll {
...
/* 红黑树中,存储着所有添加到epoll中的事件,即该epoll监控的事件(epoll_ctl将要传来的socket) */
struct rb_root rbr; // 红黑树的根节点
/* 双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件,即存储“准备就绪的事件” */
struct list_head rdllist; // 当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。
...
};
在调用epoll_create
时,内核的工作:
- 在
epoll
文件系统里建了个file
结点 - 在内核
cache
里建了个红黑树 - 再建立一个
rdllist
双向链表
所有添加到epoll
中的事件都会与设备(如网卡)驱动程序建立“回调关系”,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback
,它会把这样的事件放到上面的rdllist
双向链表中。
epoll_ctl、epoll_wait详解:
在epoll
中对于每一个事件都会建立一个epitem
结构体,如下所示:
struct epitem {
...
// 红黑树节点
/*
struct rb_node {
unsigned long __rb_parent_color; // 父节点指针和颜色信息:在最低位存储节点的颜色(0表示黑色,1表示红色),其余位存储父节点指针的地址
struct rb_node *rb_right; // 右子节点指针
struct rb_node *rb_left; // 左子节点指针
};
*/
struct rb_node rbn; // Linux内核中红黑树的节点结构体
// 双向链表节点
struct list_head rdllink;
// 与事件相关联的文件描述符信息
struct epoll_filefd ffd;
// 指向其所属的eventpoll对象
struct eventpoll *ep;
// 期待的事件类型
struct epoll_event event;
...
}; // 这里包含每一个事件对应着的信息。
-
linux内核向双向链表中,添加节点的时机。
1)当有新客户端连接即完成三次握手时,会通知服务端accept()
有返回值即listenfd
有新的读事件发生,即新客户端connfd
连接。
2)客户端close
关闭连接时,服务端read()
会返回0表示对端已关闭,即也需要close(connfd)
。
3)客户端发送过来数据时,会通知服务端connfd
有新的读事件发生,需要调用read()/recv()
。
4)服务端要发送数据时,需要调用send()/write()
。 -
当调用
epoll_wait
检查是否有发生事件的连接时,只是检查eventpoll
对象中的rdllist
双向链表是否有epitem
元素,如果不为空,则将这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_wait
效率非常高。 -
epoll_ctl
在向epoll
对象中添加、删除、修改事件时,都是基于rbtree
红黑树操作,非常快。总的来说,
epoll
是非常高效的,它可以轻易地处理百万级别的并发连接。
总结:
一颗红黑树,一张准备就绪句柄双向链表,少量的内核cache
,就帮我们解决了大并发下的socket
处理问题。
- 执行
epoll_create()
时,创建了红黑树和就绪链表; - 执行
epoll_ctl()
时,如果增加socket
句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据; - 执行
epoll_wait()
时,会立刻返回就绪链表里的数据。
ET/LT深度解析:
LT
是水平触发(默认的触发方式),属于低速模式。如果事件没有处理完,就会被一直触发。
ET
是边沿触发,属于高速模式,该事件的通知只会出现一次。
epoll监听管道的ET和LT模式:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#define maxLen 10
void sys_error(const char* str)
{
perror(str);
exit(1);
}
int main(void)
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid == -1) {
sys_error("fork error");
} else if (pid == 0) { // 子进程向管道fd[1]写端写入数据,故关闭fd[0]读端
close(fd[0]);
char buf[maxLen] = "aaaa\nbbbb\n";
for (int cnt = 0; cnt < 26; ++cnt)
{
for (int i = 0; i < maxLen - 1; ++i)
{
if (i == maxLen / 2 - 1)
{
continue;
}
buf[i] += 1;
}
write(fd[1], buf, sizeof(buf));
sleep(1);
printf("........ write one time ........\n");
}
} else if (pid > 0) { // 父进程用epoll监听管道的读端fd[0],故关闭fd[1]写端
close(fd[1]);
int epfd = epoll_create(10);
struct epoll_event event;
event.events = EPOLLIN; // LT水平触发(默认)
//event.events = EPOLLIN | EPOLLET; // ET边沿触发
event.data.fd = fd[0];
epoll_ctl(epfd, EPOLL_CTL_ADD, fd[0], &event);
struct epoll_event resevent[10]; // epoll_wait返回的就绪event,但这里只有一个需要监听的文件描述符fd[0]
while (1)
{
int ret = epoll_wait(epfd, resevent, 10, -1);
if (resevent[0].data.fd = fd[0])
{
char buf[maxLen];
ret = read(fd[0], buf, maxLen / 2);
if (ret == -1)
{
sys_error("read error");
}
write(STDOUT_FILENO, buf, ret);
}
}
close(fd[0]);
close(epfd);
}
return 0;
}
事件模型ET和LT的比较:
-
LT(Level Trigger)
水平触发(缺省的工作方式),并且同时支持block
和non-block socket
。该模式中,内核会告诉你文件描述符是否就绪,之后可对该文件描述符进行I/O
操作。如果你不做任何操作,内核还是会继续通知你。该方式出错可能性小,故传统的
select
和poll
均采用这种方式。只要有数据都会触发,缓冲区剩余未读尽的数据会导致
epoll_wait
返回。 -
ET(Edge Trigger)
边沿触发(高速工作方式),只支持non-block socket
。该模式下,当文件描述符从未就绪 --> 就绪时,内核会通过epoll
通知你。收到文件描述符已准备就绪的通知后,就不会再为文件描述符发送更多的就绪通知。只有数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致
epoll_wait
返回。注意:如果收到就绪通知后,一直不再对该文件描述符进行
I/O
操作(从而导致它再次变成未就绪状态),内核不会发送更多的通知。
举例:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <sys/epoll.h>
#define SERVER_PORT 8000
#define MAXLEN 1024
void sys_error(const char* str)
{
perror(str);
exit(1);
}
int main(void)
{
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in listen_addr;
listen_addr.sin_family = AF_INET;
listen_addr.sin_port = htons(SERVER_PORT);
listen_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(listen_sockfd, (struct sockaddr*)&listen_addr, sizeof(listen_addr));
if (ret == -1)
{
sys_error("bind error");
}
ret = listen(listen_sockfd, 128);
if (ret == -1)
{
sys_error("listen error");
}
struct epoll_event event;
struct epoll_event resevent[10];
int epfd = epoll_create(10);
/* epoll 的 ET模式(高效模式),但只支持“非阻塞模式” */
event.events = EPOLLIN | EPOLLET; // ET是边沿触发,默认为水平触发
struct sockaddr_in client_addr;
socklen_t client_addr_len;
int connect_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &client_addr_len);
char clientaddr[1024];
inet_ntop(AF_INET, &client_addr.sin_addr, clientaddr, sizeof(clientaddr));
printf("%s : %d\n", clientaddr, ntohs(client_addr.sin_port));
/* !!!修改connect_sockfd为非阻塞读 */
int flag = fcntl(connect_sockfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connect_sockfd, F_SETFL, flag);
event.data.fd = connect_sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connect_sockfd, &event); // 将connect_sockfd添加到监听红黑树中
/* 这段验证程序,只需要监听一个客户端连接的数据 */
while (1)
{
ret = epoll_wait(epfd, &resevent, 10, -1);
if (resevent[0].data.fd == connect_sockfd)
{
char buf[MAXLEN];
while ((ret = read(connect_sockfd, buf, MAXLEN / 2)) > 0) // 非阻塞、忙轮询
{
for (int i = 0; i < ret; ++i)
{
buf[i] = toupper(buf[i]);
}
write(STDOUT_FILENO, buf, ret);
}
}
}
close(listen_sockfd);
close(epfd);
return 0;
}
server.c
实现步骤:
- ·socket、bind、listen·
epoll_create
监听红黑树- 返回
epfd
epoll_ctl
添加一个监听listen_sockfd
文件描述符epoll_wait
阻塞监听,对应监听fd
有事件发生,则返回满足监听事件的数组- 判断数组中的元素是
listen_sockfd
,即有客户端要连接,则初始化connect_sockfd
的监听属性,并将其添加到红黑树上 - 判断数组中的元素不是
listen_sockfd
,则connect_sockfd
满足,即客户端发来了数据,开始执行服务器逻辑并返回结果到客户端
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define SERVER_PORT 8000
void sys_error(const char* str)
{
perror(str);
exit(1);
}
int main(void)
{
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in listen_addr;
listen_addr.sin_family = AF_INET;
listen_addr.sin_port = htons(SERVER_PORT);
listen_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(listen_sockfd, (struct sockaddr*)&listen_addr, sizeof(listen_addr));
if (ret == -1)
{
sys_error("bind error");
}
ret = listen(listen_sockfd, 128);
if (ret == -1)
{
sys_error("listen error");
}
// epfd,创建监听红黑树
int epfd = epoll_create(1024);
if (epfd == -1)
{
sys_error("epoll_create error");
}
// 初始化listen_sockfd的监听属性,并将其添加到红黑树上
struct epoll_event tmp;
tmp.events = EPOLLIN; // 默认事件模式,水平触发
tmp.data.fd = listen_sockfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sockfd, &tmp);
if (ret == -1)
{
sys_error("epoll_ctl error");
}
struct epoll_event ep[1024]; // ep为epoll_wait传出的满足监听事件的数组
while (1)
{
ret = epoll_wait(epfd, ep, 1024, -1); // 阻塞实施监听,并传出的满足监听事件的数组ep
if (ret == -1)
{
sys_error("epoll_wait error");
}
for (int i = 0; i < ret; ++i)
{
if (!(ep[i].events & EPOLLIN)) // 如果不满足“读”事件,则继续循环
{
continue;
}
// listen_sock满足读事件,表示有客户端发起连接请求
else if (ep[i].data.fd == listen_sockfd)
{
struct sockaddr_in client_addr;
socklen_t clientaddr_len;
int connect_sockfd = accept(listen_sockfd, (struct sockaddr*)&client_addr, &clientaddr_len);
char clientaddr[1024];
inet_ntop(AF_INET, &client_addr.sin_addr, clientaddr, sizeof(clientaddr));
printf("%s : %d\n", clientaddr, ntohs(client_addr.sin_port));
// 初始化connect_sockfd的监听属性,并将其添加到红黑树上
tmp.events = EPOLLIN;
tmp.data.fd = connect_sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connect_sockfd, &tmp);
}
// connect_sockfd满足读事件,表示有客户端写数据
else
{
char buf[1024];
int connect_sockfd = ep[i].data.fd;
ret = read(connect_sockfd, buf, sizeof(buf));
if (ret <= 0) {
// 表示此时 “ep[i].data.fd的对端已关闭” 或者 “出错了”,故关闭connect_sockfd,并从监听树上摘下
epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd, NULL);
close(ep[i].data.fd);
} else if (ret > 0) {
write(STDOUT_FILENO, buf, ret);
// 执行服务器的逻辑操作,并将结果返回给客户端
for (int j = 0; j < ret; ++j)
{
buf[j] = toupper(buf[j]);
}
write(STDOUT_FILENO, buf, ret);
write(connect_sockfd, buf, ret);
}
}
}
}
close(epfd);
close(listen_sockfd);
return 0;
}
epoll反应堆模型:
实现步骤:epoll ET模式
+ 非阻塞、忙轮询 + void* ptr
,需要监听connect_sockfd
的“读事件、写事件”。
socket、bind、listen
epoll_create
监听红黑树- 返回
epfd
epoll_ctl
添加一个监听listen_sockfd
文件描述符epoll_wait
阻塞监听,对应监听fd
有事件发生,则返回满足监听事件的数组- 判断数组中的元素是
listen_sockfd
,即有客户端要连接,则初始化connect_sockfd
的监听属性,并将其添加到红黑树上 - 判断数组中的元素不是
listen_sockfd
,则connect_sockfd
满足,即客户端发来了数据,执行服务器逻辑,- 将
connect_sockfd
从红黑树上摘下,EPOLLOUT
、回调函数、epoll_ctl()
,重新放回红黑树监听“写事件” - 等待
epoll_wait()
返回,说明connect_sockfd
可写,则write
结果到客户端,之后将connect_sockfd
从红黑树上摘下 EPOLLIN
、epoll_ctl()
,重新放回红黑树监听“读事件”
- 将
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#define MAX_EVENTS 1024 /* 监听上限 */
#define BUFLEN 4096 /* 缓存区大小 */
#define SERV_PORT 8000 /* 端口号 */
void recvdata(int fd, int events, void *arg);
void senddata(int fd, int events, void *arg);
/* 描述就绪文件描述符的相关信息 */
struct myevent_s
{
int fd; // 要监听的文件描述符
int events; // 对应的监听事件,EPOLLIN、EPLLOUT
void *arg; // 指向自己结构体指针
void (*call_back)(int fd,int events,void *arg); //回调函数
int status; // 是否在监听:1 -> 在红黑树上(监听)、0 -> 不在(不监听)
char buf[BUFLEN];
int len;
long last_active; // 记录每次加入红黑树 g_efd 的时间值
};
int g_efd; // 全局变量,作为红黑树根
struct myevent_s g_events[MAX_EVENTS+1]; // 自定义结构体类型数组. +1-->listen fd
// listen_sockfd放在数组的最末尾,其余为connect_sockfd
/*
* 封装一个自定义事件,包括fd、fd的回调函数、额外的参数项
* 注意:在封装这个事件的时候,为这个事件指明了回调函数,一般来说,一个fd只对一个特定的事件
* 感兴趣,当这个事件发生的时候,就调用这个回调函数
*/
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int fd,int events,void *arg), void *arg)
{
ev->fd = fd;
ev->call_back = call_back;
ev->events = 0;
ev->arg = arg;
ev->status = 0;
if(ev->len <= 0)
{
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
}
ev->last_active = time(NULL); // 调用eventset函数的时间
}
/* 向 epoll 监听的红黑树中,添加一个文件描述符 */
void eventadd(int efd, int events, struct myevent_s *ev)
{
struct epoll_event epv={0, {0}};
int op = 0;
epv.data.ptr = ev; // ptr指向一个结构体(之前的epoll模型红黑树上挂载的是文件描述符cfd和lfd,现在是ptr指针)
epv.events = ev->events = events; // EPOLLIN 或 EPOLLOUT
if(ev->status == 0) // status 说明文件描述符是否在红黑树上 0不在,1在
{
op = EPOLL_CTL_ADD; // 将其加入红黑树 g_efd, 并将status置1
ev->status = 1;
}
// 添加一个节点
if(epoll_ctl(efd, op, ev->fd, &epv) < 0) {
printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
} else {
printf("event add OK [fd=%d], events[%d]\n", ev->fd, events);
}
}
/* 从epoll 监听的 红黑树中删除一个文件描述符 */
void eventdel(int efd, struct myevent_s* ev)
{
struct epoll_event epv = {0, {0}};
if(ev->status != 1) // 如果fd没有添加到监听树上,就不用删除,直接返回
{
return;
}
epv.data.ptr = NULL;
ev->status = 0;
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);
return;
}
/* 当有文件描述符就绪, epoll返回, 调用该函数与客户端建立链接 */
void acceptconn(int listen_sockfd, int events, void* arg)
{
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int connect_sockfd, i;
if((connect_sockfd = accept(listen_sockfd, (struct sockaddr *)&cin, &len)) == -1)
{
if(errno != EAGAIN && errno != EINTR)
{
sleep(1);
}
printf("%s:accept,%s\n", __func__, strerror(errno)); // __func__用来获取函数名称
return;
}
do
{
for(i = 0; i < MAX_EVENTS; i++) // 从全局数组g_events中找一个空闲元素,类似于select中找值为-1的元素
{
if(g_events[i].status ==0)
{
break;
}
}
if(i == MAX_EVENTS) // 超出连接数上限
{
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break;
}
int flag = 0;
if((flag = fcntl(connect_sockfd, F_SETFL, O_NONBLOCK)) < 0) // 将cfd也设置为非阻塞
{
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
// 找到合适的节点之后,将其添加到监听红黑树中,并监听读事件
eventset(&g_events[i], connect_sockfd, recvdata, &g_events[i]);
eventadd(g_efd, EPOLLIN, &g_events[i]);
}while(0);
printf("new connect[%s:%d],[time:%ld],pos[%d]\n",inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
return;
}
/*读取客户端发过来的数据的函数*/
void recvdata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = recv(fd, ev->buf, sizeof(ev->buf), 0); // 读取客户端发过来的数据
eventdel(g_efd, ev); // 将该节点从红黑树上摘除
if (len > 0)
{
ev->len = len;
ev->buf[len] = '\0'; // 手动添加字符串结束标记
printf("C[%d]:%s\n", fd, ev->buf);
eventset(ev, fd, senddata, ev); // 设置该fd对应的回调函数为senddata
eventadd(g_efd, EPOLLOUT, ev); // 将fd加入红黑树g_efd中,监听其写事件
}
else if (len == 0)
{
close(ev->fd);
/* ev-g_events 地址相减得到偏移元素位置 */
printf("[fd=%d] pos[%ld], closed\n", fd, ev - g_events);
}
else
{
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return;
}
/*发送给客户端数据*/
void senddata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = send(fd, ev->buf, ev->len, 0); // 直接将数据回射给客户端
eventdel(g_efd, ev); // 从红黑树g_efd中移除
if (len > 0)
{
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
eventset(ev, fd, recvdata, ev); // 将该fd的回调函数改为recvdata
eventadd(g_efd, EPOLLIN, ev); // 重新添加到红黑树上,设为监听读事件
}
else
{
close(ev->fd); // 关闭连接
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return ;
}
/*创建 socket, 初始化listen_sockfd */
void initlistensocket(int efd, short port)
{
struct sockaddr_in sin;
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
int flag = fcntl(listen_sockfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(listen_sockfd, flag); // 将socket设为非阻塞
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_ANY);
sin.sin_port = htons(port);
bind(listen_sockfd, (struct sockaddr*)&sin, sizeof(sin));
listen(listen_sockfd, 20);
/* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */
eventset(&g_events[MAX_EVENTS], listen_sockfd, acceptconn, &g_events[MAX_EVENTS]);
/* void eventadd(int efd, int events, struct myevent_s *ev) */
eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]); // 将listen_sockfd添加到监听树上,监听读事件
return;
}
int main()
{
int port = SERV_PORT;
g_efd = epoll_create(MAX_EVENTS + 1); //创建红黑树,返回给全局 g_efd
if(g_efd <= 0)
{
printf("create efd in %s err %s\n", __func__, strerror(errno));
}
initlistensocket(g_efd, port); //初始化监听socket
struct epoll_event events[MAX_EVENTS + 1]; //定义这个结构体数组,用来接收epoll_wait传出的满足监听事件的fd结构体
printf("server running:port[%d]\n", port);
int checkpos = 0;
int i;
while(1)
{
/*
// 超时验证,每次测试100个连接,不测试listen_sockfd
// 当客户端60s内没有与服务器通信,则关闭此客户端
long now = time(NULL);
for(i=0; i < 100; i++, checkpos++)
{
if(checkpos == MAX_EVENTS);
{
checkpos = 0;
}
if(g_events[checkpos].status != 1)
{
continue;
}
long duration = now -g_events[checkpos].last_active;
if(duration >= 60)
{
close(g_events[checkpos].fd);
printf("[fd=%d] timeout\n", g_events[checkpos].fd);
eventdel(g_efd, &g_events[checkpos]);
}
}
*/
//调用eppoll_wait等待接入的客户端事件,epoll_wait传出的是满足监听条件的那些fd的struct epoll_event类型
int nfd = epoll_wait(g_efd, events, MAX_EVENTS + 1, 1000);
if (nfd < 0)
{
printf("epoll_wait error, exit\n");
exit(-1);
}
for(i = 0; i < nfd; i++)
{
// eventadd()函数中,添加监听事件到监听树的时候,会将myevents_t结构体类型给了ptr指针
// 这里epoll_wait返回的时候,在返回的events中有对应fd的myevents_t类型的指针
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
// 如果监听的是读事件,且返回的也是读事件
if ((events[i].events & EPOLLIN) &&(ev->events & EPOLLIN))
{
ev->call_back(ev->fd, events[i].events, ev->arg);
}
// 如果监听的是写事件,且返回的也是写事件
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT))
{
ev->call_back(ev->fd, events[i].events, ev->arg);
}
}
}
return 0;
}
select、epoll对比:
系统调用 | select | epoll |
---|---|---|
事件集合 | 用户通过三个参数分别传入感兴趣的可读、可写、异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件,使得每次调用select都需要重置三个参数。 | 内核通过一个(红黑树)事件表直接管理用户感兴趣的所有事件,因此每次调用epoll_wait时,不需要反复传入用户感兴趣的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件。 |
应用程序索引就绪文件描述符的时间复杂度 | O(n) | O(1) |
最大支持的文件描述符数 | 一般有最大限制值1024 | 受系统最大值限制65535 |
工作模式 | LT | 支持ET高效模式 |
内核实现和工作效率 | 采用“轮询”整个事件集合,来检测就绪事件,算法时间复杂度为O(n) | 将内核双链表中感兴趣事件集合,通过epoll_wait的传出参数获取到并从中检测就绪事件,算法时间复杂度为O(1) |
select、poll和epoll的对比:
libevent库:
libevent库介绍:
libevent源码包的安装:
-
在libevent官网下载源码包,并拷贝到
centos7
后,使用tar zxvf libevent-2.1.8-stable.tar.gz
解压到/opt
目录下。 -
源码包的安装过程:
./configure # 检查安装环境,并生成makefile文件 make # 生成.o文件和可执行文件 sudo make install # 将必要的资源放入系统指定目录 # 检查是否安装成功 cd /opt/libevent-2.1.8-stable/sample # 检查该文件下的demo是否可以正常编译和运行,即可 # 注意:编译使用该库的.c文件时,需要加上`-levent`选项,如下: gcc ./hello-world.c -o helo-world -levent # 查看库名和头文件 /usr/local/lib --> libevent.so
-
链接路径的配置:
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH # 防止error while loading shared libraries: libevent-2.1.so.6: cannot open shared object file: No such file or directory # 将`/usr/local/lib`目录添加到`LD_LIBRARY_PATH`环境变量中: # 解决方法:修改动态链接库搜索路径的配置文件 vim /etc/ld.so.conf sudo ldconfig # 更新系统的动态链接库缓存
libevent特点和组成:
libevent的特点和优势:
-
事件驱动,高性能;
-
轻量级,专注于网络;
-
跨平台,支持
Windows、Linux、Mac Os
等; -
支持多种
I/O
多路复用技术,epoll
、poll
、select
和kqueue
等;#include <event2/event.h> // 查看支持哪些多路I/O const char** event_get_supported_methods(void); // 查看当前使用的多路I/O const char* event_base_get_method(const struct event_base* base);
-
支持
I/O
,定时器和信号等事件;
注意:基于“事件”的异步通信模型(内核通过回调执行之前注册好的函数)。
libevent的组成:
- 事件管理包括各种
I/O(socket)
、定时器、信号等事件,也是libevent
应用最广的模块; - 缓存管理是指
evbuffer
功能; DNS
是libevent
提供的一个异步DNS
查询功能;HTTP
是libevent
的一个轻量级http
实现,包括服务器和客户端;
libevent框架:
#include <event2/event.h>
// 1. 创建`event_base`
struct event_base* event_base_new(void);
// 查看fork后,子进程使用的event_base
// 注意:使用该函数后,父进程创建的base才能在子进程中生效
int event_reinit(struct event_base* base);
// 返回值:成功返回0,失败返回-1
// 2. 创建事件`event`:常规事件event、带缓冲区的事件bufferevent
event_new();
bufferevent_scoket_new();
// 3. 将event添加到事件链表上,注册事件,即将事件添加到`base`上
// tv参数:设置超时时间,为NULL则一直等待事件被触发、回调函数被调用
int event_add(struct event* ev, const struct timeval* tv);
// 4. 循环、检测、分发事件,即循环监听事件满足
int event_base_dispatch(struct event_base* base);
// base:event_base_new函数的返回值
// 返回值:成功返回0,失败返回-1
/*
注意:只有event_new()中,EV_PERSIST才能持续触发,否则只触发一次就跳出循环
通常的设置为:EV_READ | EV_PERSIST、EV_WRITE | EV_PERSIST
*/
// 指定时间后,停止循环
int event_base_loopexit(struct event_base* base, const struct timeval* tv);
// 立即停止循环
int event_base_loopbreak(struct event_base* base);
// 5. 释放`event_base`
void event_base_free(struct event_base* base);
常规事件:
/* 创建一个事件 */
// what参数:
// EV_READ:一次读事件
// EV_WRITE:一次写事件
// EV_READ | EV_PERSIST(持续触发读)、EV_WRITE | EV_PERSIST(持续触发写),只有与event_base_dispatch()结合使用才能发挥作用
// cb回调函数:typedef void(*event_callback_fn)(evutil_socket_t fd, short, void*)
struct event event_new(struct event_base* base, evutil_socket_t fd, short what, event_callback_fn cb, void* arg);
/* 将事件添加到base上 */
int event_add(struct event* ev, const struct timeval* tv);
/* 将事件从base上拿下来 */
int event_del(struct event* ev);
/* 释放事件 */
int event_free(struct event* ev);
使用fifo进行读写实现:
read.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <event2/event.h>
#define BUFSIZE 1024
void sys_error(const char* str)
{
perror(str);
exit(1);
}
void read_cb(evutil_socket_t fd, short what, void* arg)
{
char buf[BUFSIZE];
int ret = read(fd, buf, sizeof(buf));
printf("%s : %s\n", what & EV_READ ? "read satisfy" : "read non-satisfy", buf);
sleep(1);
}
int main(void)
{
unlink("testfifo");
mkfifo("testfifo", 0644);
// 打开fifo命名管道的读端
int fd = open("testfifo", O_RDONLY | O_NONBLOCK);
if (fd == -1)
{
sys_error("open error");
}
// 创建event_base
struct event_base* base = event_base_new();
// 创建事件,会被持续触发写
struct event* ev = event_new(base, fd, EV_READ | EV_PERSIST, read_cb, NULL);
// 添加事件ev到base上
event_add(ev, NULL);
// 循环、检测、分发事件
event_base_dispatch(base);
// 释放资源
event_free(ev);
event_base_free(base);
close(fd);
return 0;
}
write.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <event2/event.h>
#define BUFSIZE 1024
void sys_error(const char* str)
{
perror(str);
exit(1);
}
void write_cb(evutil_socket_t fd, short what, void* arg)
{
char buf[BUFSIZE];
static int count = 0;
sprintf(buf, "hello world %d\n", count++);
printf("%s : %s\n", what & EV_WRITE ? "write satisfy" : "write non-satisfy", buf);
write(fd, buf, strlen(buf));
sleep(1);
}
int main(void)
{
// 打开fifo命名管道的读端
int fd = open("testfifo", O_WRONLY | O_NONBLOCK);
if (fd == -1)
{
sys_error("open error");
}
// 创建event_base
struct event_base* base = event_base_new();
// 创建事件,会被持续触发读
struct event* ev = event_new(base, fd, EV_WRITE | EV_PERSIST, write_cb, NULL);
// 添加事件ev到base上
event_add(ev, NULL);
// 循环、检测、分发事件
event_base_dispatch(base);
// 释放资源
event_free(ev);
event_base_free(base);
close(fd);
return 0;
}
未决和非未决:
未决态:有资格但还没有被处理
非未决态:没有资格被处理
带缓冲区的事件bufferevent
:
常用的函数:
#include <event2/listener.h>
#include <event2/bufferevent.h>
// 原理:bufferevent中有两个缓冲区(就基于队列实现,即读走就不再有数据、“先进先出”)
// 1)读缓冲区:有数据 --> 读回调函数被调用 --> 使用bufferevent_read() --> 读数据
// 2)写缓冲区:使用bufferevent_write() --> 向写缓冲中写数据 --> 该缓冲区有数据自动写出 --> 写完,回调函数被调用
/* 创建bufferevent */
// fd参数:与bufferevent绑定的文件描述符,类比event_new()
// options参数:BEV_OPT_CLOSE_ON_FREE(释放bufferevent时,关闭底层传输出端口(套接字))
struct bufferevent* bufferevent_socket_new(struct event_base* base, evutil_socket_t fd, enum bufferevent_options options);
/* 释放bufferevent */
void bufferevent_free(struct bufferevent* bev);
/* 给读写缓冲区设置回调 */
// readcb:设置bufferevent读缓冲,对应的回调(自己封装,在其内部读数据)
// writecb:设置bufferevent写缓冲,对应的回调,可为NULL
// eventcb:可传NULL
// cbarg:回调函数的参数
void bufferevent_setcb(struct bufferevent* bufev, bufferevent_data_cb readcb, bufferevent_data_cb writecb, , bufferevent_event_cb eventcb, void* cbarg);
// 1、readcb/writecb对应的回调函数
typedef void(*bufferevent_data_cb)(struct bufferevent* bev, void* ctx);
size_t bufferevent_read(struct bufferevent* bufev, void* data, size_t size); // 用来代替read
size_t bufferevent_write(struct bufferevent* bufev, const void* data, size_t size); // 用来代替write
// 2、eventcb对应的回调函数
typedef void(*bufferevent_event_cb)(struct bufferevent* bev, short events, void* ctx);
// events:不同标志位,代表不同的事件
// BEV_EVENT_READING:读取操作时发生某事件
// BEV_EVENT_WRITING:写入操作时发生某事件
// BEV_EVENT_ERROR:操作时发生错误,关于错误更多信息,EVUTIL_SOCKET_ERROR()
// BEV_EVENT_TIMEOUT:发生超时
// BEV_EVENT_EOF:遇到文件结束指示
// BEV_EVENT_CONNECTED:请求连接过程已经完成,实现客户端时可用
/* 禁用、启用缓冲区 */
// 默认:新建的bufferevent,写缓冲是enable,读缓冲是disable的
// events取值:EV_READ、EV_WRITE、EV_READ | EV_WRITE
void bufferevent_disable(struct bufferevent* bufev, short events); // 禁用缓冲区
void bufferevent_enable(struct bufferevent* bufev, short events); // 启用缓冲区
short bufferevent_get_enabled(struct bufferevent* bufev); // 获取缓冲区的禁用状态,需要借助&获取
/* 创建连接客户端 */
int bufevent_socket_connect(struct bufferevent* bev, struct sockaddr* address, int addrlen); // 替代系统调用socket()、connect()
/* 创建监听服务器 */
// cb监听回调函数:接受连接之后,执行用户要做的操作
// ptr回调函数的参数
// flags标志位:LEV_OPT_CLOSE_ON_FREE(释放bufferevent时关闭底层传输端口,即会释放底层套接字、释放底层bufferevent等)、LEV_OPT_REUSEABLE(端口可复用)、LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE
// backlog(-1表示默认最大值128) -- listen的第2个参数,即“全连接队列的长度”
// sa和port(服务端自己的地址结构和大小) -- bind()的参数
struct evconnlistener* evconnlistener_new_bind(struct event_base* base, evconnlistener_cb cb, void* ptr, unsigned flags, int backlog, const struct sockaddr* sa, int socklen); // 替代系统调用socket()、bind()、listen()、accept()
// 返回值:返回成功创建的listener
// 回调函数类型evconnlistener_cb:
// listener:evconnlistener_new_bind函数的返回值
// fd:用于通信的文件描述符
// addr、len:客户端的地址结构和addr的大小
// ptr:外部ptr传递进来的值
typedef void(*evconnlistener_cb)(struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr* addr, int len, void* ptr);
/* 释放监听服务器 */
void evconnlistener_free(struct evconnlistener* lev);
基于libevent的TCP通信实现:
server.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <event2/event.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>
#define SERVER_PORT 8000
#define BUFSIZE 1024
// 读缓冲回调函数
void read_cb(struct bufferevent* bev, void* arg)
{
// 读缓冲区中的数据
char buf[BUFSIZE];
bufferevent_read(bev, buf, sizeof(buf));
printf("from client data : %s\n", buf);
char* result = "I'm server, I have successfully received your datas.\n";
// 写数据给客户端
bufferevent_write(bev, result, strlen(result));
sleep(1);
}
// 写缓冲回调函数(bufferevent_write将数据写缓冲区后(客户端会去读),调用该回调函数)
void write_cb(struct bufferevent* bev, void* arg)
{
printf("results has been successfully sent to client\n");
}
// 事件回调函数
void event_cb(struct bufferevent* bev, short events, void* arg)
{
if (events & BEV_EVENT_EOF)
{
printf("connection closed\n");
}
if (events & BEV_EVENT_TIMEOUT)
{
printf("connection timeout\n");
}
else if (events & BEV_EVENT_ERROR)
{
printf("occur others' error\n");
}
bufferevent_free(bev);
printf("bufferevent free\n");
}
// 监听回调函数
void cb_listener(struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr* addr, int len, void* ptr)
{
printf("connect new client\n");
struct event_base* base = (struct event_base*)ptr;
// 创建bufferevent对象
struct bufferevent* bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
// 给bufferevent缓冲区设置回调函数
bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
// 启用bufferevent的读缓冲(默认为disable)
bufferevent_enable(bev, EV_READ);
}
int main(void)
{
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 创建event_base
struct event_base* base = event_base_new();
// 创建监听服务器(创建套接字、绑定、接收连接情趣):实现了这四个函数的功能,socket()创建套接字、bind()绑定、listen()监听、accept()接收客户端的连接请求
struct evconnlistener* listener = evconnlistener_new_bind(base, cb_listener, base, LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, 10, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 循环、监听、分发:
event_base_dispatch(base);
evconnlistener_free(listener);
event_base_free(base);
return 0;
}
client.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
#define SERVER_PORT 8000
#define SERVER_IP "127.0.0.1"
#define BUFSIZE 1024
// 读缓冲回调函数
void read_cb(struct bufferevent* bev, void* arg)
{
// 读缓冲区中的数据
char buf[BUFSIZE] = {0};
bufferevent_read(bev, buf, sizeof(buf));
printf("from server data : %s\n", buf);
char* result = "I'm client, your results has been successfully received\n";
// 写数据给服务端
bufferevent_write(bev, result, strlen(result));
sleep(1);
}
// 写缓冲回调函数(bufferevent_write将数据写缓冲区后(客户端会去读),调用该回调函数)
void write_cb(struct bufferevent* bev, void* arg)
{
printf("results has been successfully sent to server\n");
}
// 事件回调函数:用来处理连接成功、错误事件
void event_cb(struct bufferevent* bev, short events, void* arg)
{
if (events & BEV_EVENT_EOF)
{
printf("connection closed\n");
}
else if (events & BEV_EVENT_ERROR)
{
printf("occur others' error\n");
}
else if (events & BEV_EVENT_CONNECTED)
{
printf("connect successfully\n");
return;
}
bufferevent_free(bev);
printf("bufferevent free\n");
}
void read_terminal(evutil_socket_t fd, short what, void* arg)
{
char buf[BUFSIZE] = {0};
int ret = read(fd, buf, sizeof(buf));
// 将从客户端fd中,读到的数据写入给bufferevent的write缓冲区,发送给服务端
struct bufferevent* bev = (struct bufferevent*)arg;
bufferevent_write(bev, buf, ret);
}
int main(void)
{
struct event_base* base = event_base_new();
int client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 通信fd放在bufferevent中
struct bufferevent* bev = bufferevent_socket_new(base, client_sockfd, BEV_OPT_CLOSE_ON_FREE);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr.s_addr);
// 连接服务器
bufferevent_socket_connect(bev, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 设置回调函数
bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
/* 注意:该函数对程序执行的影响!!! */
// 启用bufferevent的读缓冲(默认为disable)(注销该语句,则read_cb()函数不能被调用)
//bufferevent_enable(bev, EV_READ); // 添加该语句,会导致bufferevent读缓冲中一旦有数据,则会一直触发读回调函数
// 创建事件:用来获取“终端的读事件”
struct event* ev = event_new(base, STDIN_FILENO, EV_READ | EV_PERSIST, read_terminal, bev);
// 添加事件
event_add(ev, NULL); // 不设置超时时间
// 循环、检测、分发:
event_base_dispatch(base);
event_free(ev);
event_base_free(base);
return 0;
}