day14
多路IO转接:
select:
poll:
相比 select 相差不多
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-----fds:监听的文件描述符【数组】
------struct pollfd {
int fd: 待监听的文件描述符
short events: 待监听的文件描述符对应的监听事件
取值:POLLIN、POLLOUT、POLLERR
可读事件:
POLLIN:有数据可读。
POLLPRI:有高优先级数据可读。
可写事件:
POLLOUT:可以写数据。
错误事件:
POLLERR:发生错误。
POLLHUP:挂起事件。
POLLNVAL:无效的文件描述符。
short revnets: 传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR
}
------该结构体返回值:
> 0:返回准备就绪的文件描述符数量。
0:超时,没有任何文件描述符变为可用。
-1:出错,设置 errno。
nfds: 监听数组的,实际有效监听个数。
timeout: > 0: 超时时长。单位:毫秒。
-1: 阻塞等待
0: 不阻塞
返回值:返回满足对应监听事件的文件描述符 总个数。
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。events 和 revents
select 里 没有分离, 使用的是 传入传出参数, 因此需要备份
拓展 监听上限。 超出 1024限制。
缺点:
不能跨平台。 Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。
补充-1 poll实例(略,见课件)
注意 按位与
read 函数返回值:
> 0: 实际读到的字节数
=0: socket中,表示对端关闭。close()
-1: 如果 errno == EINTR 被异常终端。 需要重启。
如果 errno == EAGIN 或 EWOULDBLOCK 以非阻塞方式读数据,但是没有数据。 需要,再次读。
如果 errno == ECONNRESET 说明连接被 重置。 需要 close(),移除监听队列。
错误。
突破 1024 文件描述符限制:
cat /proc/sys/fs/file-max --> 当前计算机所能打开的最大文件个数。 受硬件影响。
9223372036854775807 是 Linux 系统中允许打开的文件描述符的最大数量。这个值是一个 64 位整数的最大值(即 2^63 - 1),表示系统理论上可以支持的文件描述符数量几乎没有限制。
ulimit -a ――> 当前用户下的进程,默认打开文件描述符个数。 缺省为 1024
修改:
打开 sudo vi /etc/security/limits.conf, 写入:
* soft nofile 65536 --> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】
* hard nofile 100000 --> 命令修改上限。
或者
ulimit -n 值
epoll(大重点):
红黑树!!!
非常适合 3,500,1000 这样的 监听数, 不需要轮询
使用三大函数:
epoll_create
epoll_ctl
epoll_wait
epoll_create
int epoll_create(int size); 创建一棵监听红黑树
size:创建的红黑树的监听节点数量。(仅供内核参考。预计)
返回值:指向新创建的红黑树的根节点的 fd。
失败: -1 errno
epoll_ctl(主要重点)
epoll_ctl
并不会存储指针本身,而是将指针指向的内存区域的内容拷贝到epoll
内部的数据结构中。所以即使在init_listen_fd
函数退出后,ev
变量的作用域结束,epoll
内部已经有了它的副本。
详见 http 代码, 局部变量
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作监听红黑树(ctrl)
epfd:epoll_create 函数的返回值。 epfd
op:对该监听红黑数所做的操作。
EPOLL_CTL_ADD 添加fd到 监听红黑树
EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。
EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
fd:
待监听的fd
event: 本质 struct epoll_event 结构体 地址
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
成员 events:
EPOLLIN / EPOLLOUT / EPOLLERR
成员 data: 联合体(共用体):
int fd; 对应监听事件的 fd
void *ptr;
uint32_t u32;
uint64_t u64;
返回值:成功 0; 失败: -1 errno
联合体
联合体(Union) 是 C 语言中的一种特殊数据类型,允许在相同的内存位置存储不同的数据类型。联合体的所有成员共享同一块内存空间,因此同一时间只能存储其中一个成员的值。联合体的大小由其最大成员的大小决定。
epoll_wait
区别上下函数的 event(结构体) 和 events(数组)
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞监听。
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); // tep是公用的, 每次代表牌对应的 fd的属性
} 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模式:Edge Triggered
边沿触发: 在触发一次后, 假如有剩余数据, 将不会读出, 而是必须再有事件发生,才能继续读
缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
LT模式:Level Triggered
水平触发 -- 默认采用模式。
缓冲区剩余未读尽的数据会导致 epoll_wait 返回。
结论:
epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 — 忙轮询。
struct epoll_event event;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);
int flg = fcntl(cfd, F_GETFL);
flg |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flg);
优点:
高效。突破1024文件描述符。
缺点:
不能跨平台。 Linux。
ET 阻塞的原因
1. ET 模式的行为
- 在 ET 模式下,
epoll
只会在文件描述符的状态发生变化时通知用户程序。- 例如,如果数据到达套接字,
epoll
只会通知一次,即使缓冲区中还有未读取的数据。
- 例如,如果数据到达套接字,
- 如果没有一次性读取完所有数据,
epoll
不会再次通知,除非有新的数据到达。
2. 阻塞模式的问题
- 如果文件描述符是阻塞的(blocking),在读取数据时可能会发生以下情况:
- 当
epoll
通知有数据可读时,用户程序调用read
读取数据。 - 如果数据没有一次性读完,
read
会阻塞,直到有新的数据到达。 - 由于 ET 模式不会再次通知,程序可能会一直阻塞在
read
调用中,无法处理其他事件。
- 当
但是, 这个阻塞 也是有用处的
误区
注意: epoll,select,poll 并不是只能监听 socket
epoll
、select
和 poll
不仅可以监听 socket,还可以监听其他类型的文件描述符,比如:
- 标准输入/输出(stdin/stdout)
- 管道(pipe)
- 普通文件
- 设备文件
- 信号量
- 定时器(如
timerfd
) - 信号事件(如
signalfd
)
这些机制的核心是 I/O 多路复用,它们可以监控任何支持 read
/write
操作的文件描述符。
补充-2 epoll 监听pipe(LT和ET)
#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <errno.h>
#include <unistd.h>
#define MAXLINE 10
int main(int argc, char *argv[])
{
int efd, i;
int pfd[2];
pid_t pid;
char buf[MAXLINE], ch = 'a';
pipe(pfd);
pid = fork();
if (pid == 0) { //子 写
close(pfd[0]);
while (1) {
//aaaa\n
for (i = 0; i < MAXLINE/2; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//bbbb\n
for (; i < MAXLINE; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//aaaa\nbbbb\n
write(pfd[1], buf, sizeof(buf));
sleep(5); // 用于测试 边缘触发
}
close(pfd[1]);
} else if (pid > 0) { //父 读
struct epoll_event event;
struct epoll_event resevent[10]; //epoll_wait就绪返回event
int res, len;
close(pfd[1]);
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; // ET 边沿触发
// event.events = EPOLLIN; // LT 水平触发 (默认)
event.data.fd = pfd[0];
epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);
while (1) {
res = epoll_wait(efd, resevent, 10, -1);
printf("res %d\n", res);
if (resevent[0].data.fd == pfd[0]) {
len = read(pfd[0], buf, MAXLINE/2);
write(STDOUT_FILENO, buf, len);
}
}
close(pfd[0]);
close(efd);
} else {
perror("fork");
exit(-1);
}
return 0;
}
补充-3 epoll-server-client
server
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#define MAXLINE 10
#define SERV_PORT 9000
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int efd;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
struct epoll_event event;
struct epoll_event resevent[10];
int res, len;
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; /* ET 边沿触发 */
//event.events = EPOLLIN; /* 默认 LT 水平触发 */
printf("Accepting connections ...\n");
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
event.data.fd = connfd;
epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);
while (1) {
res = epoll_wait(efd, resevent, 10, -1);
printf("res %d\n", res);
if (resevent[0].data.fd == connfd) {
len = read(connfd, buf, MAXLINE/2); //readn(500)
write(STDOUT_FILENO, buf, len);
}
}
return 0;
}
client
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define MAXLINE 10
#define SERV_PORT 9000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, i;
char ch = 'a';
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (1) {
//aaaa\n
for (i = 0; i < MAXLINE/2; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//bbbb\n
for (; i < MAXLINE; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//aaaa\nbbbb\n
write(sockfd, buf, sizeof(buf));
sleep(5);
}
close(sockfd);
return 0;
}
补充-4 epoll-ET-非阻塞
ET一般结合 非阻塞去进行
使用fcntl函数,设置 文件描述符 非阻塞
flag = fcntl(connfd, F_GETFL); /* 修改connfd为非阻塞读 */
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#define MAXLINE 10
#define SERV_PORT 8000
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int efd, flag;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
///
struct epoll_event event;
struct epoll_event res_event[10];
int res, len;
efd = epoll_create(10);
event.events = EPOLLIN | EPOLLET; /* ET 边沿触发,默认是水平触发 */
//event.events = EPOLLIN;
printf("Accepting connections ...\n");
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
flag = fcntl(connfd, F_GETFL); /* 修改connfd为非阻塞读 */
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
event.data.fd = connfd;
epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event); //将connfd加入监听红黑树
while (1) {
printf("epoll_wait begin\n");
res = epoll_wait(efd, res_event, 10, -1); //最多10个, 阻塞监听
printf("epoll_wait end res %d\n", res);
if (res_event[0].data.fd == connfd) {
while ((len = read(connfd, buf, MAXLINE/2)) >0 ) //非阻塞读, 轮询
write(STDOUT_FILENO, buf, len);
}
}
return 0;
}
epoll 反应堆模型:
重点是 灵活使用 联合体的 *ptr
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();
epoll反应堆实例(能看懂就行了)
/*
*epoll基于非阻塞I/O事件驱动
*/
#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 8080
void recvdata(int fd, int events, void *arg);
void senddata(int fd, int events, void *arg);
/* 描述就绪文件描述符相关信息 */
struct myevent_s {
int fd; //要监听的文件描述符
int events; //对应的监听事件
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; //全局变量, 保存epoll_create返回的文件描述符
struct myevent_s g_events[MAX_EVENTS+1]; //自定义结构体类型数组. +1-->listen fd
/*将结构体 myevent_s 成员变量 初始化*/
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg)
{
ev->fd = fd;
ev->call_back = call_back;
ev->events = 0;
ev->arg = arg;
ev->status = 0;
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
ev->last_active = time(NULL); //调用eventset函数的时间
return;
}
/* 向 epoll监听的红黑树 添加一个 文件描述符 */
//eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
void eventadd(int efd, int events, struct myevent_s *ev)
{
struct epoll_event epv = {0, {0}};
int op;
epv.data.ptr = ev;
epv.events = ev->events = events; //EPOLLIN 或 EPOLLOUT
if (ev->status == 0) { //已经在红黑树 g_efd 里
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], op=%d, events[%0X]\n", ev->fd, op, events);
return ;
}
/* 从epoll 监听的 红黑树中删除一个 文件描述符*/
void eventdel(int efd, struct myevent_s *ev)
{
struct epoll_event epv = {0, {0}};
if (ev->status != 1) //不在红黑树上
return ;
//epv.data.ptr = ev;
epv.data.ptr = NULL;
ev->status = 0; //修改状态
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv); //从红黑树 efd 上将 ev->fd 摘除
return ;
}
/* 当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */
void acceptconn(int lfd, int events, void *arg)
{
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) {
if (errno != EAGAIN && errno != EINTR) {
/* 暂时不做出错处理 */
}
printf("%s: accept, %s\n", __func__, strerror(errno));
return ;
}
do {
for (i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素
if (g_events[i].status == 0) //类似于select中找值为-1的元素
break; //跳出 for
if (i == MAX_EVENTS) {
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break; //跳出do while(0) 不执行后续代码
}
int flag = 0;
if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) { //将cfd也设置为非阻塞
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
/* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */
eventset(&g_events[i], cfd, recvdata, &g_events[i]);
eventadd(g_efd, EPOLLIN, &g_events[i]); //将cfd添加到红黑树g_efd中,监听读事件
} 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); //读文件描述符, 数据存入myevent_s成员buf中
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, 初始化lfd */
void initlistensocket(int efd, short port)
{
struct sockaddr_in sin;
int lfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(lfd, F_SETFL, O_NONBLOCK); //将socket设为非阻塞
memset(&sin, 0, sizeof(sin)); //bzero(&sin, sizeof(sin))
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(port);
bind(lfd, (struct sockaddr *)&sin, sizeof(sin));
listen(lfd, 20);
/* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */
eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);
/* void eventadd(int efd, int events, struct myevent_s *ev) */
eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
return ;
}
int main(int argc, char *argv[])
{
unsigned short port = SERV_PORT;
if (argc == 2)
port = atoi(argv[1]); //使用用户指定端口.如未指定,用默认端口
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]; //保存已经满足就绪事件的文件描述符数组
printf("server running:port[%d]\n", port);
int checkpos = 0, i;
while (1) {
/* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */
long now = time(NULL); //当前时间
for (i = 0; i < 100; i++, checkpos++) { //一次循环检测100个。 使用checkpos控制检测对象
if (checkpos == MAX_EVENTS)
checkpos = 0;
if (g_events[checkpos].status != 1) //不在红黑树 g_efd 上
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]); //将该客户端 从红黑树 g_efd移除
}
}
/*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/
int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
if (nfd < 0) {
printf("epoll_wait error, exit\n");
break;
}
for (i = 0; i < nfd; i++) {
/*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr; // 把 ptr强转成了 自定义结构体, 经过赋值, ev和ptr, 都指向ptr,且是自定义结构体类型
if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) { //读就绪事件 将 events数组的 events和 其data里的ptr 里面的 自定义结构体的 events 都设置成 读监听
/*
struct myevent_s {
int fd; //要监听的文件描述符
int events; //对应的监听事件
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 的时间值
};
*/
ev->call_back(ev->fd, events[i].events, ev->arg);
//lfd EPOLLIN
}
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) { //写就绪事件
ev->call_back(ev->fd, events[i].events, ev->arg);
}
}
}
/* 退出前释放所有资源 */
return 0;
}
day15
aptitude命令
它是 apt
的高级封装,提供了更多功能和更友好的用户界面(包括命令行和交互式界面)。
ctags工具
ctags
是一种用于为程序生成标签文件的工具,它支持多种编程语言。标签文件包含源代码中的函数、变量、类等标识符的位置,这样开发者可以快速在代码中跳转到定义或声明的位置。
在大型项目中,ctags
是非常有用的,尤其是在使用编辑器(如 Vim、Emacs)时,可以结合它们的插件或功能实现高效的代码导航。
vim使用:
跳转到光标下的标识符定义处:
Ctrl + ]
返回之前的位置:
Ctrl + t
线程池
对于 I/O多路复用 和 多线程, 多路I/O复用, 效率 比 多线程,多进程高
减少线程/进程切换开销:
- 多线程和多进程需要频繁进行上下文切换,特别是在高并发情况下,上下文切换的开销会迅速增加。
- 多路 I/O 复用在一个线程内处理所有 I/O,避免了上下文切换,节省了 CPU 时间。
多路 i/o转接 + 服务端线程池, 效率高
线程池主要结构体
struct threadpool_t {
pthread_mutex_t lock; /* 用于锁住本结构体 */
pthread_mutex_t thread_counter; /* 记录忙状态线程个数de琐 -- 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; / 各子线程任务结构体 */
rear = 5 % 5
线程池模块分析:
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();
线程池代码示例(自己看容易理解)
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include "threadpool.h"
#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
typedef struct {
void *(*function)(void *); /* 函数指针,回调函数 */
void *arg; /* 上面函数的参数 */
} threadpool_task_t; /* 各子线程任务结构体 */
/* 描述线程池相关信息 */
struct threadpool_t {
pthread_mutex_t lock; /* 用于锁住本结构体 */
pthread_mutex_t thread_counter; /* 记录忙状态线程个数de琐 -- 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 */
};
void *threadpool_thread(void *threadpool);
void *adjust_thread(void *threadpool);
int is_thread_alive(pthread_t tid);
int threadpool_free(threadpool_t *pool);
//threadpool_create(3,100,100);
threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size)
{
int i;
threadpool_t *pool = NULL; /* 线程池 结构体 */ //局部的结构体变量
do {
if((pool = (threadpool_t *)malloc(sizeof(threadpool_t))) == NULL) {
printf("malloc threadpool fail");
break; /*跳出do while*/
}
pool->min_thr_num = min_thr_num;
pool->max_thr_num = max_thr_num;
pool->busy_thr_num = 0;
pool->live_thr_num = min_thr_num; /* 活着的线程数 初值=最小线程数 */
pool->wait_exit_thr_num = 0;
pool->queue_size = 0; /* 有0个产品 */
pool->queue_max_size = queue_max_size; /* 最大任务队列数 */
pool->queue_front = 0;
pool->queue_rear = 0;
pool->shutdown = false; /* 不关闭线程池 */
/* 根据最大线程上限数, 给工作线程数组开辟空间, 并清零 */
pool->threads = (pthread_t *)malloc(sizeof(pthread_t)*max_thr_num);
if (pool->threads == NULL) {
printf("malloc threads fail");
break;
}
memset(pool->threads, 0, sizeof(pthread_t)*max_thr_num);
/* 给 任务队列 开辟空间 */
pool->task_queue = (threadpool_task_t *)malloc(sizeof(threadpool_task_t)*queue_max_size);
if (pool->task_queue == NULL) {
printf("malloc task_queue fail");
break;
}
/* 初始化互斥琐、条件变量 */
if (pthread_mutex_init(&(pool->lock), NULL) != 0
|| pthread_mutex_init(&(pool->thread_counter), NULL) != 0
|| pthread_cond_init(&(pool->queue_not_empty), NULL) != 0
|| pthread_cond_init(&(pool->queue_not_full), NULL) != 0)
{
printf("init the lock or cond fail");
break;
}
/* 启动 min_thr_num 个 work thread */
for (i = 0; i < min_thr_num; i++) { //这里开始实际 创建线程
pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool); /*pool指向当前线程池*/
printf("start thread 0x%x...\n", (unsigned int)pool->threads[i]);
}
pthread_create(&(pool->adjust_tid), NULL, adjust_thread, (void *)pool); /* 创建管理者线程 */
return pool;
} while (0);
threadpool_free(pool); /* 前面代码调用失败时,释放poll存储空间 */
return NULL;
}
/* 向线程池中 添加一个任务 */
//threadpool_add(thp, process, (void*)&num[i]); /* 向线程池中添加任务 process: 小写---->大写*/
int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg)
{
pthread_mutex_lock(&(pool->lock));
/* ==为真,队列已经满, 调wait阻塞 */
while ((pool->queue_size == pool->queue_max_size) && (!pool->shutdown)) {
pthread_cond_wait(&(pool->queue_not_full), &(pool->lock));
}
if (pool->shutdown) {
pthread_cond_broadcast(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));
return 0;
}
/* 清空 工作线程 调用的回调函数 的参数arg */
if (pool->task_queue[pool->queue_rear].arg != NULL) {
pool->task_queue[pool->queue_rear].arg = NULL;
}
/*添加任务到任务队列里*/
pool->task_queue[pool->queue_rear].function = function;
pool->task_queue[pool->queue_rear].arg = arg;
pool->queue_rear = (pool->queue_rear + 1) % pool->queue_max_size; /* 队尾指针移动, 模拟环形 */
pool->queue_size++;
/*添加完任务后,队列不为空,唤醒线程池中 等待处理任务的线程*/
pthread_cond_signal(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));
return 0;
}
/* 线程池中各个工作线程 */
void *threadpool_thread(void *threadpool)
{
threadpool_t *pool = (threadpool_t *)threadpool;
threadpool_task_t task;
while (true) {
/* Lock must be taken to wait on conditional variable */
/*刚创建出线程,等待任务队列里有任务,否则阻塞等待任务队列里有任务后再唤醒接收任务*/
pthread_mutex_lock(&(pool->lock));
/*queue_size == 0 说明没有任务,调 wait 阻塞在条件变量上, 若有任务,跳过该while*/
while ((pool->queue_size == 0) && (!pool->shutdown)) {
printf("thread 0x%x is waiting\n", (unsigned int)pthread_self());
pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));
/*清除指定数目的空闲线程,如果要结束的线程个数大于0,结束线程*/
if (pool->wait_exit_thr_num > 0) {
pool->wait_exit_thr_num--;
/*如果线程池里线程个数大于最小值时可以结束当前线程*/
if (pool->live_thr_num > pool->min_thr_num) {
printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
pool->live_thr_num--;
pthread_mutex_unlock(&(pool->lock));
pthread_exit(NULL);
}
}
}
/*如果指定了true,要关闭线程池里的每个线程,自行退出处理---销毁线程池*/
if (pool->shutdown) {
pthread_mutex_unlock(&(pool->lock));
printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
pthread_detach(pthread_self());
pthread_exit(NULL); /* 线程自行结束 */
}
/*从任务队列里获取任务, 是一个出队操作*/
task.function = pool->task_queue[pool->queue_front].function;
task.arg = pool->task_queue[pool->queue_front].arg;
pool->queue_front = (pool->queue_front + 1) % pool->queue_max_size; /* 出队,模拟环形队列 */
pool->queue_size--;
/*通知可以有新的任务添加进来*/
pthread_cond_broadcast(&(pool->queue_not_full));
/*任务取出后,立即将 线程池琐 释放*/
pthread_mutex_unlock(&(pool->lock));
/*执行任务*/
printf("thread 0x%x start working\n", (unsigned int)pthread_self());
pthread_mutex_lock(&(pool->thread_counter)); /*忙状态线程数变量琐*/
pool->busy_thr_num++; /*忙状态线程数+1*/
pthread_mutex_unlock(&(pool->thread_counter));
(*(task.function))(task.arg); /*执行回调函数任务*/
//task.function(task.arg); /*执行回调函数任务*/
/*任务结束处理*/
printf("thread 0x%x end working\n", (unsigned int)pthread_self());
pthread_mutex_lock(&(pool->thread_counter));
pool->busy_thr_num--; /*处理掉一个任务,忙状态数线程数-1*/
pthread_mutex_unlock(&(pool->thread_counter));
}
pthread_exit(NULL);
}
/* 管理线程 */
void *adjust_thread(void *threadpool)
{
int i;
threadpool_t *pool = (threadpool_t *)threadpool;
while (!pool->shutdown) {
sleep(DEFAULT_TIME); /*定时 对线程池管理*/
pthread_mutex_lock(&(pool->lock));
int queue_size = pool->queue_size; /* 关注 任务数 */
int live_thr_num = pool->live_thr_num; /* 存活 线程数 */
pthread_mutex_unlock(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
int busy_thr_num = pool->busy_thr_num; /* 忙着的线程数 */
pthread_mutex_unlock(&(pool->thread_counter));
/* 创建新线程 算法: 任务数大于最小线程池个数, 且存活的线程数少于最大线程个数时 如:30>=10 && 40<100*/
if (queue_size >= MIN_WAIT_TASK_NUM && live_thr_num < pool->max_thr_num) {
pthread_mutex_lock(&(pool->lock));
int add = 0;
/*一次增加 DEFAULT_THREAD 个线程*/
for (i = 0; i < pool->max_thr_num && add < DEFAULT_THREAD_VARY
&& pool->live_thr_num < pool->max_thr_num; i++) {
if (pool->threads[i] == 0 || !is_thread_alive(pool->threads[i])) {
pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool);
add++;
pool->live_thr_num++;
}
}
pthread_mutex_unlock(&(pool->lock));
}
/* 销毁多余的空闲线程 算法:忙线程X2 小于 存活的线程数 且 存活的线程数 大于 最小线程数时*/
if ((busy_thr_num * 2) < live_thr_num && live_thr_num > pool->min_thr_num) {
/* 一次销毁DEFAULT_THREAD个线程, 隨機10個即可 */
pthread_mutex_lock(&(pool->lock));
pool->wait_exit_thr_num = DEFAULT_THREAD_VARY; /* 要销毁的线程数 设置为10 */
pthread_mutex_unlock(&(pool->lock));
for (i = 0; i < DEFAULT_THREAD_VARY; i++) {
/* 通知处在空闲状态的线程, 他们会自行终止*/
pthread_cond_signal(&(pool->queue_not_empty));
}
}
}
return NULL;
}
int threadpool_destroy(threadpool_t *pool)
{
int i;
if (pool == NULL) {
return -1;
}
pool->shutdown = true;
/*先销毁管理线程*/
pthread_join(pool->adjust_tid, NULL);
for (i = 0; i < pool->live_thr_num; i++) {
/*通知所有的空闲线程*/
pthread_cond_broadcast(&(pool->queue_not_empty));
}
for (i = 0; i < pool->live_thr_num; i++) {
pthread_join(pool->threads[i], NULL);
}
threadpool_free(pool);
return 0;
}
int threadpool_free(threadpool_t *pool)
{
if (pool == NULL) {
return -1;
}
if (pool->task_queue) {
free(pool->task_queue);
}
if (pool->threads) {
free(pool->threads);
pthread_mutex_lock(&(pool->lock));
pthread_mutex_destroy(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
pthread_mutex_destroy(&(pool->thread_counter));
pthread_cond_destroy(&(pool->queue_not_empty));
pthread_cond_destroy(&(pool->queue_not_full));
}
free(pool);
pool = NULL;
return 0;
}
int threadpool_all_threadnum(threadpool_t *pool)
{
int all_threadnum = -1; // 总线程数
pthread_mutex_lock(&(pool->lock));
all_threadnum = pool->live_thr_num; // 存活线程数
pthread_mutex_unlock(&(pool->lock));
return all_threadnum;
}
int threadpool_busy_threadnum(threadpool_t *pool)
{
int busy_threadnum = -1; // 忙线程数
pthread_mutex_lock(&(pool->thread_counter));
busy_threadnum = pool->busy_thr_num;
pthread_mutex_unlock(&(pool->thread_counter));
return busy_threadnum;
}
int is_thread_alive(pthread_t tid)
{
int kill_rc = pthread_kill(tid, 0); //发0号信号,测试线程是否存活
if (kill_rc == ESRCH) {
return false;
}
return true;
}
/*测试*/
#if 1
/* 线程池中的线程,模拟处理业务 */
void *process(void *arg)
{
printf("thread 0x%x working on task %d\n ",(unsigned int)pthread_self(),(int)arg);
sleep(1); //模拟 小---大写
printf("task %d is end\n",(int)arg);
return NULL;
}
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;
}
#endif
使用do-while 达成 go-to的技巧
do{
break;
}while(0)
注意
1.锁的静态初始化
局部变量不能用于锁的静态初始化,因为锁的静态初始化要求在编译时确定锁的初始状态,而局部变量的生命周期和作用域仅限于函数调用期间,在函数栈上分配,无法支持这种初始化方式。
编译时初始化:静态初始化如 PTHREAD_MUTEX_INITIALIZER
是在编译阶段完成的,这就要求变量必须是全局或静态的,才能确保在程序开始时就已经完成初始化。
2.void*隐式转换
malloc
返回的是一个 void *
指针(无类型指针)。在 C 中,void *
可以隐式转换为任意类型的指针,因此 在纯 C 中通常不需要强制类型转换。
C++ 中,void *
不能隐式转换为其他类型指针,必须显式强制类型转换
TCP通信和UDP通信各自的优缺点:
TCP: 面向连接的,可靠数据包传输。对于不稳定的网络层,采取完全弥补的通信方式。 丢包重传。
优点:
稳定。
数据流量稳定、速度稳定、顺序
缺点:
传输速度慢。相率低。开销大。
使用场景:数据的完整型要求较高,不追求效率。
大数据传输、文件传输。
UDP: 无连接的,不可靠的数据报传递。对于不稳定的网络层,采取完全不弥补的通信方式。 默认还原网络状况
优点:
传输速度块。相率高。开销小。
缺点:
不稳定。
数据流量。速度。顺序。
使用场景:对时效性要求较高场合。稳定性其次。
游戏、视频会议、视频电话。 腾讯、华为、阿里 --- 应用层数据校验协议,弥补udp的不足。
recv,send,recvfrom,sendto,read,write
recv
函数
recv
函数用于从已连接的套接字接收数据。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd:已连接的套接字描述符。
buf:指向接收数据的缓冲区的指针。
len:缓冲区的大小(以字节为单位)。
flags:控制接收行为的标志位,通常设置为 0。常见的标志包括:
MSG_PEEK:查看数据但不从缓冲区移除。
MSG_WAITALL:等待所有请求的数据到达。
返回值:
成功时返回接收到的字节数。
返回 0 表示连接已关闭。
返回 -1 表示出错,错误码存储在 errno 中。
send
函数
send
函数用于向已连接的套接字发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
sockfd:已连接的套接字描述符。
buf:指向要发送的数据的缓冲区的指针。
len:要发送的数据的字节数。
flags:控制发送行为的标志位,通常设置为 0。常见的标志包括:
MSG_DONTWAIT:非阻塞发送。
MSG_OOB:发送带外数据。
返回值:
成功时返回发送的字节数。
返回 -1 表示出错,错误码存储在 errno 中。
recv
和 send
:
- 专门用于**套接字(socket)**通信。
- 适用于网络编程,支持 TCP、UDP 等协议。
- 提供了额外的标志参数(如
MSG_PEEK
、MSG_OOB
等),可以控制更细粒度的行为。
read
和 write
:
- 是通用的文件 I/O 函数,适用于所有文件描述符(包括文件、管道、套接字等)。
- 在套接字上使用时,功能与
recv
和send
类似,但没有额外的标志参数。
recvfrom
和 sendto
是用于 无连接套接字(如 UDP 套接字)的网络编程函数。它们与 recv
和 send
的主要区别在于,recvfrom
和 sendto
可以指定或获取数据的目标地址或来源地址,因此适用于无连接的通信协议(如 UDP)。
recvfrom
函数
在UDP中,涵盖了 accept函数的功能
recvfrom
用于从无连接套接字(如 UDP 套接字)接收数据,并获取发送方的地址信息。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数:
sockfd:套接字描述符。
buf:指向接收数据的缓冲区的指针。
len:缓冲区的大小(以字节为单位)。
flags:控制接收行为的标志位,通常设置为 0。常见的标志包括:
MSG_PEEK:查看数据但不从缓冲区移除。
MSG_WAITALL:等待所有请求的数据到达。
src_addr:指向 struct sockaddr 的指针,用于存储发送方的地址信息。
addrlen:指向 socklen_t 的指针,表示 src_addr 的长度。调用前需要初始化为 src_addr 的大小。
返回值:
成功时返回接收到的字节数。
返回 0 表示对端发送了一个空数据包(UDP 中可能发生)。
返回 -1 表示出错,错误码存储在 errno 中。
sendto
函数
在UDP中,涵盖了 connet函数功能!
sendto
用于向无连接套接字(如 UDP 套接字)发送数据,并指定目标地址。
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。常见的标志包括:
MSG_DONTWAIT:非阻塞发送。
MSG_OOB:发送带外数据。
dest_addr:指向 struct sockaddr 的指针,表示目标地址。
addrlen:dest_addr 的长度。
返回值:
成功时返回发送的字节数。
返回 -1 表示出错,错误码存储在 errno 中。
注意事项
- UDP 是无连接的:
recvfrom
和sendto
适用于 UDP 套接字,因为 UDP 是无连接的,每次通信都需要指定目标地址或获取来源地址。- 如果用于 TCP 套接字,
recvfrom
和sendto
的行为与recv
和send
类似,但地址参数会被忽略。
- 地址结构:
recvfrom
和sendto
使用struct sockaddr
作为地址参数,实际使用时需要根据协议族(如 IPv4 或 IPv6)转换为具体的地址结构(如struct sockaddr_in
或struct sockaddr_in6
)。
- 阻塞与非阻塞:
- 在阻塞模式下,
recvfrom
会一直等待直到有数据到达,sendto
会等待直到数据被发送。 - 在非阻塞模式下,如果没有数据可接收或发送缓冲区已满,它们会立即返回 -1,并设置
errno
为EAGAIN
或EWOULDBLOCK
。
- 在阻塞模式下,
- 数据包边界:
- UDP 是面向数据报的协议,
recvfrom
和sendto
会保留数据包的边界。每次调用recvfrom
会接收一个完整的数据包,每次调用sendto
会发送一个完整的数据包。
- UDP 是面向数据报的协议,
UDP实现的 C/S 模型:
recv()/send() 只能用于 TCP 通信。 替代 read、write
accpet(); ---- Connect(); ---被舍弃 listen(); --- 可有可无 listen是为 监听准备的
server:
lfd = socket(AF_INET, SOCK_STREAM, 0); SOCK_DGRAM --- 报式协议。
bind();
listen(); --- 可有可无
while(1){
read(cfd, buf, sizeof) --- 被替换 --- recvfrom() --- 涵盖accept传出地址结构。
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: 对端关闭。
小-- 大
write();--- 被替换 --- sendto()---- connect
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
}
close();
client:
connfd = socket(AF_INET, SOCK_DGRAM, 0);
sendto(‘服务器的地址结构’, 地址结构大小)----- 将TCP的 connet()函数,换成这个, 直接发地址
recvfrom()
写到屏幕
close();
补充-1 udp实现 服务器,客户端
nc 命令使用的是 tcp 协议, 因此 无法使用nc命令 连接 udp服务端,
// server
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#define SERV_PORT 8000
int main(void)
{
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
int sockfd;
char buf[BUFSIZ];
char str[INET_ADDRSTRLEN];
int i, n;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_PORT);
bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
printf("Accepting connections ...\n");
while (1) {
clie_addr_len = sizeof(clie_addr);
n = recvfrom(sockfd, buf, BUFSIZ,0, (struct sockaddr *)&clie_addr, &clie_addr_len);
if (n == -1)
perror("recvfrom error");
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&clie_addr, sizeof(clie_addr));
if (n == -1)
perror("sendto error");
}
close(sockfd);
return 0;
}
// client
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
int sockfd, n;
char buf[BUFSIZ];
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, BUFSIZ, stdin) != NULL) {
n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (n == -1)
perror("sendto error");
n = recvfrom(sockfd, buf, BUFSIZ, 0, NULL, 0); //NULL:不关心对端信息
if (n == -1)
perror("recvfrom error");
write(STDOUT_FILENO, buf, n);
}
close(sockfd);
return 0;
}
bind 绑定的,都是自己的
udp 没有 connect 去绑定服务端, 因此 是 用 sendto 进行绑定服务端
补充-2 strlen和sizeof
1. strlen
-
功能:
strlen
是一个标准库函数,用于计算字符串的长度(不包括终止符\0
)。 -
头文件:
<string.h>
-
返回值: 返回字符串的长度(
size_t
类型)。 -
示例:
#include <stdio.h> #include <string.h> int main() { char str[] = "Hello, World!"; size_t len = strlen(str); printf("Length of the string: %zu\n", len); // 输出: 13 return 0; }
2. sizeof
-
功能:
sizeof
是一个操作符,用于计算数据类型或变量所占用的内存大小(以字节为单位)。 -
返回值: 返回数据类型或变量的大小(
size_t
类型)。 -
示例:
#include <stdio.h> int main() { char str[] = "Hello, World!"; size_t size = sizeof(str); printf("Size of the array: %zu\n", size); // 输出: 14 (包括终止符 '\0') return 0; }
主要区别
strlen
:- 仅适用于字符串。
- 计算字符串的实际长度(不包括
\0
)。 - 运行时计算。
sizeof
:- 适用于所有数据类型和变量。
- 计算数据类型或变量占用的内存大小(包括
\0
对于字符数组)。 - 编译时计算。****
示例对比
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello";
printf("strlen(str): %zu\n", strlen(str)); // 输出: 5
printf("sizeof(str): %zu\n", sizeof(str)); // 输出: 6 (包括 '\0')
return 0;
}
总结
- 使用
strlen
获取字符串长度。 - 使用
sizeof
获取数据类型或变量的大小。
本地套接字 domain:
Interprocess Communication,进程间通信 IPC: pipe、fifo、mmap、信号、本地套(domain)--- CS模型
对比网络编程 TCP C/S模型, 注意以下几点:
1. int socket(int domain, int type, int protocol); 参数 domain:AF_INET(ipv4) --> 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”); // 不使用 2+ 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");
这个 表述有点错误, bind 是将已经创建的 socket 与一个地址关联起来。 而不是 创建
4. 客户端不能依赖 “隐式绑定”。并且应该在通信建立过程中,创建且初始化2个地址结构:
1) client_addr --> bind()
2) server_addr --> connect();
网络套接字, 隐式绑定 自己的 地址结构, 因为 系统会随机分配
但是本地套接字, 不能隐式绑定 自己的地址结构, 需要写明 本地的 套接字地址文件名
struct sockaddr_un
的定义
struct sockaddr_un
是用于 Unix 域套接字(Unix Domain Socket)的地址结构体。Unix 域套接字是一种进程间通信(IPC)机制,允许在同一台机器上的进程之间进行高效的数据传输。与网络套接字不同,Unix 域套接字不经过网络协议栈,而是直接通过文件系统进行通信。
struct sockaddr_un
的定义通常位于 <sys/un.h>
头文件中,其结构如下:
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; // 地址族,固定为 AF_UNIX 或 AF_LOCAL
char sun_path[108]; // 文件路径名(用于标识套接字)
};
sun_family
: 地址族,必须设置为AF_UNIX
或AF_LOCAL
(两者等价)。sun_path
: 一个文件路径名,用于标识套接字。路径名的长度通常不能超过 108 字节。
offsetof
本节可以用这个函数 测定 sa_family_t sun_family 的大小
offsetof
是一个 C 语言标准库宏,定义在 <stddef.h>
头文件中。它用于计算结构体中某个成员的偏移量(即该成员相对于结构体起始地址的字节偏移量)。
#include <stddef.h>
size_t offsetof(type, member);
type
: 结构体类型。member
: 结构体中的成员名称。
返回值
offsetof
返回 size_t
类型的值,表示指定成员相对于结构体起始地址的字节偏移量。
unlink的应用
unlink()
用于删除文件系统中的文件。它的行为如下:
- 删除文件:
- 如果
pathname
是一个文件的路径,unlink()
会删除该文件。 - 如果文件是打开的,文件内容仍然可以被访问,直到所有打开的文件描述符都被关闭。
- 如果
- 删除硬链接:
- 如果文件有多个硬链接,
unlink()
只会删除指定的链接,文件内容仍然存在,直到所有硬链接都被删除。
- 如果文件有多个硬链接,
- 不能删除目录:
unlink()
不能用于删除目录。删除目录需要使用rmdir()
或remove()
。
特别注意 char []:
虽然你能通过 char a[10] = "hello";
这样给字符数组赋值,但这只是数组初始化的一种形式。它是通过编译器在定义数组时自动处理的,并且只在声明时有效。
在 struct sockaddr_un
中,sun_path
是一个已存在的字符数组,不是你声明的数组。所以你不能直接通过 =
给 sun_path
赋值。
补充-3 domain 实例
// server
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>
#include "wrap.h"
#define SERV_ADDR "serv.socket"
int main(void)
{
int lfd, cfd, len, size, i;
struct sockaddr_un servaddr, cliaddr;
char buf[4096];
lfd = Socket(AF_UNIX, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path, SERV_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path); /* servaddr total len */
unlink(SERV_ADDR); /* 确保bind之前serv.sock文件不存在,bind会创建该文件 */
Bind(lfd, (struct sockaddr *)&servaddr, len); /* 参3不能是sizeof(servaddr) */
Listen(lfd, 20);
printf("Accept ...\n");
while (1) {
len = sizeof(cliaddr); //AF_UNIX大小+108B
cfd = Accept(lfd, (struct sockaddr *)&cliaddr, (socklen_t *)&len);
len -= offsetof(struct sockaddr_un, sun_path); /* 得到文件名的长度 */
cliaddr.sun_path[len] = '\0'; /* 确保打印时,没有乱码出现 */
printf("client bind filename %s\n", cliaddr.sun_path);
while ((size = read(cfd, buf, sizeof(buf))) > 0) {
for (i = 0; i < size; i++)
buf[i] = toupper(buf[i]);
write(cfd, buf, size);
}
close(cfd);
}
close(lfd);
return 0;
}
client 需要确定两次 地址结构
一次用于 bind 绑定 自己的 地址结构
一次用于 connect 服务器的 地址结构
// client
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <strings.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <sys/un.h>
#include <stddef.h>
#include "wrap.h"
#define SERV_ADDR "serv.socket"
#define CLIE_ADDR "clie.socket"
int main(void)
{
int cfd, len;
struct sockaddr_un servaddr, cliaddr;
char buf[4096];
cfd = Socket(AF_UNIX, SOCK_STREAM, 0);
bzero(&cliaddr, sizeof(cliaddr));
cliaddr.sun_family = AF_UNIX;
strcpy(cliaddr.sun_path,CLIE_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(cliaddr.sun_path); /* 计算客户端地址结构有效长度 */
unlink(CLIE_ADDR);
Bind(cfd, (struct sockaddr *)&cliaddr, len); /* 客户端也需要bind, 不能依赖自动绑定*/
bzero(&servaddr, sizeof(servaddr)); /* 构造server 地址 */
servaddr.sun_family = AF_UNIX;
strcpy(servaddr.sun_path, SERV_ADDR);
len = offsetof(struct sockaddr_un, sun_path) + strlen(servaddr.sun_path); /* 计算服务器端地址结构有效长度 */
Connect(cfd, (struct sockaddr *)&servaddr, len);
while (fgets(buf, sizeof(buf), stdin) != NULL) {
write(cfd, buf, strlen(buf));
len = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
}
close(cfd);
return 0;
}
对比本地套 和 网络套。
网络套接字 本地套接字
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);
serv_addr.sin_port = htons(8888); strcpy(serv_addr.sun_path, "套接字文件名")
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);