【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.编程过程中需要注意的地方:
- 当select返回的时候,说明有事件发生了(但是不知道是哪一个事件),因此需要调用for()+FD_ISSET()先判断
- 如果对端的服务器已经断开了,多进程、线程服务器直接调用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>
- 创建底座:
struct event_base *base = event_base_new();
- 创建事件:
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,
- 添加事件到底座base上:
int event_add(struct event *ev, const struct timeval *tv);
- ev: event_new的返回值
- tv:为NULL:一直等到事件被触发 回调函数会被调用;为非0:没有事件触发,时间到了,回调函数依旧被调用
- 启动循环:
int event_base_dispatch(struct event_base *base); 内部就是while(1){epoll}
- 释放事件:
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);
}