【Linux 网络开发】 黑马Linux网络编程学到的东西

注意:本文主要概括了学到的【要点】: 就是学习过程中难点和重点,没有包罗万象,全部罗列。

如果想要系统学习和复盘,请参考以下资料:

1. B站视频《黑马程序员-Linux网络编程》:https://www.bilibili.com/video/BV1iJ411S7UA?p=1

2. 语雀:一份写的很全面、详细的笔记:https://www.yuque.com/boyhui/ukcpwf/ieg4t7k1d32hvbvb#BtyyJ

 

网络基础

C/S模型、B/S模型对比:

侦听socket与新创建socket的区别:

 

 

一个疑问:无法把文件描述符(fd)转换为文件结构体指针(FILE *)

 

 

实验1:多进程并发服务器编写

1. 用于父进程回收子进程的信号处理

当子进程结束的时候,会给父进程发送 SIGCHLD 的信号。父进程可以利用此信号【中断式地】回收子进程(而不用像wait()函数阻塞等待子进程结束)。

//SIGCHLD信号的处理函数
static void sigchld_handler(int sig)
{
     /* 替子进程收尸 */
     printf("父进程回收子进程\n");
     while (waitpid(-1, NULL, WNOHANG) > 0)
     continue;
}

/*绑定信号与其信号处理函数*/
static int bind_sigact(int signum, void (*sa_handler)(int))
{
    struct sigaction sig_act = {0};

     sigemptyset(&(sig_act.sa_mask));
     sig_act.sa_handler = sa_handler;
     sig_act.sa_flags = 0;


    if (-1 == sigaction(signum, &sig_act, NULL))  /* 为 SIGCHLD 信号绑定处理函数 */
    {
         perror("sigaction error");
         exit(-1);
    }
    return 0;
}

 2.一边accpet() 一边fork()

父进程循环调用accpet(), 并调用fork()创建子进程

for(int i=0; i<SKT_NUM; i++)
{
    cnt_fd[i] = Accept(skt_fd, (struct sockaddr *)(&client_sock[i]), &addrlen);

    session_pid = fork();
    switch(session_pid)
    {
        case -1:
            {
                perror("fork failed!");
                exit(-1);
            }
        case 0://子进程:应该执行读、转换、写工作,可以封装成一个函数
            {
                comm_process(cnt_fd[i], skt_fd);
                _exit(0);
            }
            break;
        default://父进程:继续进行accpet工作
            {
                continue;
            }
    }
}

 

实验2:多线程并发服务器编写

read函数返回值的总结

1.>0:表示读到数据的实际字节数。

2. 等于0时,表示读到文件末尾。(对于socket编程来说,就是对端已经关闭链接)

如上图所示,当服务器读到0时,并不是服务器的读缓存没有发现数据,而是由于TCP四次”挥手“之后,这部分缓存已经关闭,所以读到0.

另外,socket()编程中的 receive()返回值也具有这个意义。

3.返回值小于0时,返回-1且设置errno

  • 当errno = EINTR,表示被信号中断,并且对信号的处理方式为捕捉。对于read函数处理方式可以选择重启或退出。
  • errno = EAGAIN 或者 EWOULDBLOCK,表示以非阻塞方式读,但是数据到达。
  • errno为其他值时,表示异常,可以执行perror 、exit。

read()一般应用如下:

int ret = 0;
again:
    ret = recv(temp_fd, server_buf, sizeof(server_buf), 0);
if(ret < 0)
{
    if(errno == EINTR || errno == EAGAIN)
    {
        goto again;
    }
    else
    {
        close(temp_fd);
        perror("recv/read failed!");
        pthread_exit(NULL);
    }
}
else if(ret == 0)
{
    close(temp_fd);
    printf("The client %d is closing...\n", temp_fd);
    pthread_exit(NULL);
}
else
//read()返回正常值时执行的程序

 

 

实验3:select()函数实现并发服务器编写

函数原型:

//int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//返回值:
// >0 有监听的事件产生
//==0 超时
// == -1 有错误

3-1. 为什么select 能够完成多线程、多进程的任务呢?

与多线程、多进程的一直阻塞等待不同,select()是响应式实现服务器工作的。

当有相应的事件发生的时候,服务器才会做出相应的动作。这一点有点像单片机中的中断,也有点像Linux中的信号中断,但是select()函数是没有中断号、中断处理函数的概念的。

因此需要自己做以下三件事情:

  • 自己设置要监视的事件
  • 自己判断事件是否发没发生(select无法直接定位满足监听事件的文件描述符,单片机的中断不用自己判断)
  • 判断完成后,自己实现相应的处理。

3-2.编程过程中需要注意的地方:

  1. 当select返回的时候,说明有事件发生了(但是不知道是哪一个事件),因此需要调用for()+FD_ISSET()先判断
  2. 如果对端的服务器已经断开了,多进程、线程服务器直接调用pthread_exit()或者exit()函数退出就可以。而select()相关处理则是使用FD_CLR清除掉相应的文件描述符的位图。
sqlist *cnt_fd_listp;
cnt_fd_listp = list_create();//数据结构,创建顺序表


for(int i=0; i<=cnt_fd_listp->last; i++)//挨个检查判断哪个文件描述符发生变化
{	
    int temp_fd = cnt_fd_listp->data[i];
    if(FD_ISSET(temp_fd, &rd_set))
    {
        ret = read_and_reply(temp_fd);
        if(ret < 0) //表示当前对端的服务器已经断开
        {
            close(temp_fd);
            FD_CLR(temp_fd, &orig_set);//注意这里!!!使用FD_CLR清除掉相应的文件描述符的位图
            list_data_delete(cnt_fd_listp, temp_fd);
        }
    }
}

3-3. select()函数的不足

 

实验4:epoll()函数实现并发服务器编写

epoll()函数的优点:

a。epoll_wait()直接返回的nready和struct epoll_event *events就是产生事件的fd,
    不用自己一个一个判断,也不需要自己编写数据结构。

b。当删除的时候,使用epoll_ctl+EPOLL_CTL_DEL很方便,不用自己维护一个数据结构。

 

如何修改文件描述符限制(默认:1024)

 

 

实验5:epoll reactor基础

epoll_ctl()函数和struct epoll_event 结构体

poll()pollfd结构体对比, epoll_ctl()中的struct epoll_event结构体与之很相似。

在使用上:区别有以下几点:

  • epoll_ctl()中的struct epoll_event结构体有共用体结构体成员,可用其中的void * ptr 实现回调函数。
  • epoll_wait()返回只返回监听事件发生的成员,而poll()传出参数全部返回,需要自己判断。

 

 

 
 
 

TCP和UDP协议对比

nc命令作为udp客户端

使用nc命令创建UDP客户端时,需要指定目标主机的IP地址或主机名、监听的端口号等参数。

 nc -u <server_ip> <port> (注意加里面的-u选项,否则默认作为tcp客户端)

 

实验6 本地套接字的客户端

 

 

实验7 libevent 初识——实现TCP服务器

1. libevent 的源码包安装、编译使用选项说明

2. 普通event的编程过程:

头文件:#include <event2/event.h> 

  1. 创建底座:struct event_base *base = event_base_new();
  2. 创建事件:struct event *event_new(struct event_base *base, evutil_socket_t fd, short what, event_callback_fn callback, void *arg)
  • base:event_base_new的返回值,也就是底座
  • fd:绑定到事件上的文件描述符
  • what:监听的事件(EV_READ、EV_WRITE、EV_PERSIST(持续触发))
  • callback:一旦事件满足监听条件,回调的函数
  • typedef void (*event_callback_fn函数名)(evutil_socket_t fd,short what ,void *arg)
  • arg:回调函数的参数
  • 返回值:成功--返回创建的event,
  1. 添加事件到底座base上:int event_add(struct event *ev, const struct timeval *tv);
  • ev: event_new的返回值
  • tv:为NULL:一直等到事件被触发 回调函数会被调用;为非0:没有事件触发,时间到了,回调函数依旧被调用
  1. 启动循环:int event_base_dispatch(struct event_base *base); 内部就是while(1){epoll}
  2. 释放事件:int event_free(struct event *ev);
  • ev: event_new的返回值

 

 

3. bufferevent的编程过程:

图 bufferevent 编程流程

  • 创建底座event_base(和上面一样): struct event_base *base = event_base_new();
  • 创建服务器连接监听器(上面没有这部分,需要自己写):相当于传统socket编程中的socket()+bind()+listen()+accept()。
    struct evconnlistener *evconnlistener_new_bind (struct event_base *base,evconnlistener_cb cb, void *ptr, unsigned flags, int backlog, const struct sockaddr *sa, int socklen);
    这个函数的参数可以分为

bind()部分: 

    • sa:服务器自己的地址结构体
    • socklen:服务器自己地址结构体的大小

listen()部分:

    • backlog:listen()的参数二,传-1表示最大值

回调函数部分(相当于作用是accept()的回调):

    • cb:回调函数-->一旦被回调,说明在其内部应该与客户端完成数据读写操作 进行通信;
    • ptr:回调函数的参数,可以将base传进去,在回调函数中bufferevent_socket_new时要用

其他参数:

    • base:底座
    • flags:LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE
    • 返回值:成功创建的监听器

上面回调函数触发的条件是有新的客户端连接上来(类似于epoll()反应堆中,给侦听套接字——lfd设置了读回调,就是这么用的,尽管很奇怪~~~哈呀)

 当有新的客户端连接上来时,回调函数被执行。会得到一个fd(本质是连接套接字——cfd, 用于下面事件创建里面的fd的输入参数)。

  • 创建事件(和上面的普通event 用一个event_new()就同时封装fd和设置其回调不同)而这里的 bufferevent 需要以下两个函数:

    使用bufferevent_socket_new()创建一个新的bufferevent事件,将fd封装到这个事件对象中,struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options);

    • base:event_base(底座)
    • fd:文件描述符,此处有歧义:应该传入的是与客户端连接的套接字描述符 cfd,而不是用于监听的套接字描述符 lfd
    • options:BEV_OPT_CLOSE_ON_FREE
    • 返回:成功创建的 bufferevent事件对象

    然后再用bufferevent_setcb() 设置这个事件对象的相关回调。void bufferevent_setcb(struct bufferevent * bufev, readcb,writecb, eventcb,void *cbarg );

    • bufev:bufferevent_socket_new的返回值(bufferevent事件对象)
    • readcb:设置buffeverent读缓冲的对应回调 read_cb { bufferevent_read() 读数据 }
    • writecb:设置buffeverent写缓冲的对应回调 write_cb {} -->给调用者发送写成功的通知,可传NULL
    • eventcb:设置事件回调,也可传NULL
    • cbarg:上述回调函数的参数

     

  • 启动循环侦听(和上面一样):event_base_dispatch(base);
  • 释放事件(和上面差不多):void bufferevent_socket_free(struct bufferevent *bev);

    bev:bufferevent_socket_new的返回值(bufferevent事件对象)

 

 

4. 补充:回调函数的具体写法和深入认识

 首先,回调函数参数的传递是libevent 自己完成的,并不需要自己来传(这需要和原来自己编写的epoll反应堆区别开来)

下面的listener()的回调函数,用于完成accpet()的工作:

/*
*@func:evconnlistener_new_bind()的第二个参数,此函数不用用户自己调用
*@param:
    fd——是传入参数,本质是通信描述符——cfd, 由 libevent 自动传入。
    (这里说明,执行此回调函数的时候,已经accpet返回了cfd了。)
    
    addr——客户端的地址结构
    void *ptr——监听器传进来的base
*/
void listener_cb(struct evconnlistener *listener,
                 evutil_socket_t fd,
                 struct sockaddr *addr,
                 int len, void *ptr)
{
    printf("connect new client");

    // 接收监听器传进来的base——这里为什么可以这样写呢????
    /*
    为了让这里能够正常使用,需要在主函数中,调用evconnlistener_new_bind()的第三个参数里面
    填入event_base *类型的指针,才能正常使用,代码如下所示:
        struct event_base *base = event_base_new();
        
        struct evconnlistener *listener;
        listener = evconnlistener_new_bind(base, listener_cb, base,
         LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1, (struct sockaddr *)&serve_addr, sizeof(serve_addr));
    */
    struct event_base *base = (struct event_base *)ptr;

    // 创建bev对象,用于监听客户端的读写事件
    struct bufferevent *bev;
    bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);

    // 设置bev对象的读写回调, 并开启读权限
    bufferevent_setcb(bev, read_cb, write_cb, NULL, NULL);
    bufferevent_enable(bev, EV_READ);
}

其次,bufferevent读缓存默认关闭,而写缓存默认开启,因此读写之前,需要事先在监听器的回调中调用 bufferevent_enable(bev, EV_READ);中使能 bufferevent 的读缓存

另外,下面是bufferevent的读相关的回调函数原型,用于完成具体的事务:

void read_cb(struct bufferevent *bev, void *arg)
{
    ...
 }   

其中,在bufferevent的读写相关的回调函数中,因为回调函数的参数里面,没有cfd了(取而代之的是*bev),因此不能使用传统的read,write(),而用以下两个函数代替

  • size_t bufferevent_read(struct bufferenvt *bev, void *buf,  size_t bufsize);
  • size_t bufferevent_write(struct bufferenvt *bev, const void *buf,  size_t bufsize);

另外,还有一个eventcb, 用于设置事件回调,常用的写法如下:

void event_cb(struct bufferevent *bev, short events, void *arg)
{
    if (events & BEV_EVENT_EOF)
    {
        printf("connection closed\n");
    }
    else if (events & BEV_EVENT_ERROR)
    {
        printf("some other error\n");
    }
    else if (events & BEV_EVENT_CONNECTED)
    {
        printf("已经连接服务器...\\(^o^)/...\n");
        return;
    }
    //  释放资源
    bufferevent_free(bev);
}

 

posted @ 2024-02-15 17:35  FBshark  阅读(130)  评论(0编辑  收藏  举报