linux网络编程(二)

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

epollselectpoll 不仅可以监听 socket,还可以监听其他类型的文件描述符,比如:

  1. 标准输入/输出(stdin/stdout)
  2. 管道(pipe)
  3. 普通文件
  4. 设备文件
  5. 信号量
  6. 定时器(如 timerfd
  7. 信号事件(如 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 中。

recvsend

  • 专门用于**套接字(socket)**通信。
  • 适用于网络编程,支持 TCP、UDP 等协议。
  • 提供了额外的标志参数(如 MSG_PEEKMSG_OOB 等),可以控制更细粒度的行为。

readwrite

  • 是通用的文件 I/O 函数,适用于所有文件描述符(包括文件、管道、套接字等)。
  • 在套接字上使用时,功能与 recvsend 类似,但没有额外的标志参数。

recvfromsendto 是用于 无连接套接字(如 UDP 套接字)的网络编程函数。它们与 recvsend 的主要区别在于,recvfromsendto 可以指定或获取数据的目标地址或来源地址,因此适用于无连接的通信协议(如 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 中。

注意事项

  1. UDP 是无连接的
    • recvfromsendto 适用于 UDP 套接字,因为 UDP 是无连接的,每次通信都需要指定目标地址或获取来源地址。
    • 如果用于 TCP 套接字,recvfromsendto 的行为与 recvsend 类似,但地址参数会被忽略。
  2. 地址结构
    • recvfromsendto 使用 struct sockaddr 作为地址参数,实际使用时需要根据协议族(如 IPv4 或 IPv6)转换为具体的地址结构(如 struct sockaddr_instruct sockaddr_in6)。
  3. 阻塞与非阻塞
    • 在阻塞模式下,recvfrom 会一直等待直到有数据到达,sendto 会等待直到数据被发送。
    • 在非阻塞模式下,如果没有数据可接收或发送缓冲区已满,它们会立即返回 -1,并设置 errnoEAGAINEWOULDBLOCK
  4. 数据包边界
    • UDP 是面向数据报的协议,recvfromsendto 会保留数据包的边界。每次调用 recvfrom 会接收一个完整的数据包,每次调用 sendto 会发送一个完整的数据包。

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_UNIXAF_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() 用于删除文件系统中的文件。它的行为如下:

  1. 删除文件:
    • 如果 pathname 是一个文件的路径,unlink() 会删除该文件。
    • 如果文件是打开的,文件内容仍然可以被访问,直到所有打开的文件描述符都被关闭。
  2. 删除硬链接:
    • 如果文件有多个硬链接,unlink() 只会删除指定的链接,文件内容仍然存在,直到所有硬链接都被删除。
  3. 不能删除目录:
    • 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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值