01P-复习-Linux网络编程
02P-信号量生产者复习
03P-协议
协议:
一组规则。
04P-7层模型和4层模型及代表协议
分层模型结构:
OSI七层模型: 物、数、网、传、会、表、应TCP/IP 4层模型:网(链路层/网络接口层)、网、传、应应用层:http、ftp、nfs、ssh、telnet。。。传输层:TCP、UDP网络层:IP、ICMP、IGMP链路层:以太网帧协议、ARP
05P-网络传输数据封装流程
网络传输流程:
数据没有封装之前,是不能在网络中传递。数据-》应用层-》传输层-》网络层-》链路层 --- 网络环境
06P-以太网帧和ARP请求
以太网帧协议:
ARP协议:根据 Ip 地址获取 mac 地址。以太网帧协议:根据mac地址,完成数据包传输。
07P-IP协议
IP协议:
版本: IPv4、IPv6 -- 4位TTL: time to live 。 设置数据包在路由节点中的跳转上限。每经过一个路由节点,该值-1, 减为0的路由,有义务将该数据包丢弃源IP: 32位。--- 4字节 192.168.1.108 --- 点分十进制 IP地址(string) --- 二进制 目的IP:32位。--- 4字节
08P-端口号和UDP协议
UDP:
16位:源端口号。 2^16 = 65536
16位:目的端口号。
IP地址:可以在网络环境中,唯一标识一台主机。
端口号:可以网络的一台主机上,唯一标识一个进程。
ip地址+端口号:可以在网络环境中,唯一标识一个进程。
09P-TCP协议
TCP协议:
16位:源端口号。 2^16 = 65536 16位:目的端口号。32序号;32确认序号。 6个标志位。16位窗口大小。 2^16 = 65536
10P-BS和CS模型对比
c/s模型:
client-server
b/s模型:
browser-serverC/S B/S优点: 缓存大量数据、协议选择灵活 安全性、跨平台、开发工作量较小速度快缺点: 安全性、跨平台、开发工作量较大 不能缓存大量数据、严格遵守 http
11P-套接字
网络套接字: socket
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)在通信过程中, 套接字一定是成对出现的。
12P-回顾
13P-网络字节序
网络字节序:
小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678大端法:(网络存储) 高位存低地址。地位存高地址。htonl --> 本地--》网络 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序htons --> 本地--》网络 (port)ntohl --> 网络--》 本地(IP)ntohs --> 网络--》 本地(Port)
14P-IP地址转换函数
IP地址转换函数:
int inet_pton(int af, const char *src, void *dst); 本地字节序(string IP) ---> 网络字节序af:AF_INET、AF_INET6src:传入,IP地址(点分十进制)dst:传出,转换后的 网络字节序的 IP地址。 返回值:成功: 1异常: 0, 说明src指向的不是一个有效的ip地址。失败:-1const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序 ---> 本地字节序(string IP)af:AF_INET、AF_INET6src: 网络字节序IP地址dst:本地字节序(string IP)size: dst 的大小。返回值: 成功:dst。 失败:NULL
15P-sockaddr地址结构
sockaddr地址结构: IP + port –> 在网络环境中唯一标识一个进程。
struct sockaddr_in addr;addr.sin_family = AF_INET/AF_INET6 man 7 ipaddr.sin_port = htons(9527);int dst;inet_pton(AF_INET, "192.157.22.45", (void *)&dst);addr.sin_addr.s_addr = dst;【*】addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系统中有效的任意IP地址。二进制类型。bind(fd, (struct sockaddr *)&addr, size);
16P-socket模型创建流程分析
17P-socket和bind
socket函数:
#include <sys/socket.h>int socket(int domain, int type, int protocol); 创建一个 套接字domain:AF_INET、AF_INET6、AF_UNIXtype:SOCK_STREAM、SOCK_DGRAMprotocol: 0 返回值:成功: 新套接字所对应文件描述符失败: -1 errno
#include <arpa/inet.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个 地址结构 (IP+port)sockfd: socket 函数返回值struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(8888);addr.sin_addr.s_addr = htonl(INADDR_ANY);addr: 传入参数(struct sockaddr *)&addraddrlen: sizeof(addr) 地址结构的大小。返回值:成功:0失败:-1 errno
18P-listen和accept
int listen(int sockfd, int backlog); 设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
sockfd: socket 函数返回值backlog:上限数值。最大值 128.返回值:成功:0失败:-1 errno
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。
sockfd: socket 函数返回值addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)socklen_t clit_addr_len = sizeof(addr);addrlen:传入传出。 &clit_addr_len入:addr的大小。 出:客户端addr实际大小。返回值:成功:能与客户端进行数据通信的 socket 对应的文件描述。失败: -1 , errno
19P-connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用现有的 socket 与服务器建立连接
sockfd: socket 函数返回值struct sockaddr_in srv_addr; // 服务器地址结构srv_addr.sin_family = AF_INET;srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致。inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);addr:传入参数。服务器的地址结构addrlen:服务器的地址结构的大小返回值:成功:0失败:-1 errno如果不使用bind绑定客户端地址结构, 采用"隐式绑定".
20P-CS模型的TCP通信分析
TCP通信流程分析:
server:1. socket() 创建socket2. bind() 绑定服务器地址结构3. listen() 设置监听上限4. accept() 阻塞监听客户端连接5. read(fd) 读socket获取客户端数据6. 小--大写 toupper()7. write(fd)8. close();client:1. socket() 创建socket2. connect(); 与服务器建立连接3. write() 写数据到 socket4. read() 读转换后的数据。5. 显示读取结果6. close()
21P-server的实现
代码如下:
- #include <stdio.h>
- #include <ctype.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- #define SERV_PORT 9527
- void sys_err(const char *str)
- {
-
perror(str);
-
exit(1);
- }
- int main(int argc, char *argv[])
- {
-
int lfd = 0, cfd = 0;
-
int ret, i;
-
char buf[BUFSIZ], client_IP[1024];
-
struct sockaddr_in serv_addr, clit_addr; // 定义服务器地址结构 和 客户端地址结构
-
socklen_t clit_addr_len; // 客户端地址结构大小
-
serv_addr.sin_family = AF_INET; // IPv4
-
serv_addr.sin_port = htons(SERV_PORT); // 转为网络字节序的 端口号
-
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 获取本机任意有效IP
-
lfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个 socket
-
if (lfd == -1) {
-
sys_err("socket error");
-
}
-
bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));//给服务器socket绑定地址结构(IP+port)
-
listen(lfd, 128); // 设置监听上限
-
clit_addr_len = sizeof(clit_addr); // 获取客户端地址结构大小
-
cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len); // 阻塞等待客户端连接请求
-
if (cfd == -1)
-
sys_err("accept error");
-
printf("client ip:%s port:%dn",
-
inet_ntop(AF_INET, &clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)),
-
ntohs(clit_addr.sin_port)); // 根据accept传出参数,获取客户端 ip 和 port
-
while (1) {
-
ret = read(cfd, buf, sizeof(buf)); // 读客户端数据
-
write(STDOUT_FILENO, buf, ret); // 写到屏幕查看
-
for (i = 0; i < ret; i++) // 小写 -- 大写
-
buf[i] = toupper(buf[i]);
-
write(cfd, buf, ret); // 将大写,写回给客户端。
-
}
-
close(lfd);
-
close(cfd);
-
return 0;
- }
编译测试,结果如下:
22P-获取客户端地址结构
cfd = accept(lfd, (struct sockaddr *)&clit_addr, &clit_addr_len);
accept函数中的clit_addr传出的就是客户端地址结构,IP+port
于是,在代码中增加此段代码,可获取客户端信息:
printf(“client ip:%s port:%dn”,
inet_ntop(AF_INET,&clit_addr.sin_addr.s_addr, client_IP, sizeof(client_IP)),
ntohs(clit_addr.sin_port));
上一节代码中已经有这段代码,这里就不再跑一遍了。
23P-client的实现
- #include <stdio.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- #define SERV_PORT 9527
- void sys_err(const char *str)
- {
-
perror(str);
-
exit(1);
- }
- int main(int argc, char *argv[])
- {
-
int cfd;
-
int conter = 10;
-
char buf[BUFSIZ];
-
struct sockaddr_in serv_addr; //服务器地址结构
-
serv_addr.sin_family = AF_INET;
-
serv_addr.sin_port = htons(SERV_PORT);
-
//inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
-
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
-
cfd = socket(AF_INET, SOCK_STREAM, 0);
-
if (cfd == -1)
-
sys_err("socket error");
-
int ret = connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
-
if (ret != 0)
-
sys_err("connect err");
-
while (--conter) {
-
write(cfd, "hellon", 6);
-
ret = read(cfd, buf, sizeof(buf));
-
write(STDOUT_FILENO, buf, ret);
-
sleep(1);
-
}
-
close(cfd);
-
return 0;
- }
编译运行,结果如下:
这里遇到过一个问题,如果之前运行server,用Ctrl+z终止进程,ps aux列表里会有服务器进程残留,这个会影响当前服务器。解决方法是kill掉这些服务器进程。不然端口被占用,当前运行的服务器进程接收不到东西,没有回显。
24P-总结
协议:
一组规则。
分层模型结构:
OSI七层模型: 物、数、网、传、会、表、应TCP/IP 4层模型:网(链路层/网络接口层)、网、传、应应用层:http、ftp、nfs、ssh、telnet。。。传输层:TCP、UDP网络层:IP、ICMP、IGMP链路层:以太网帧协议、ARP
c/s模型:
client-server
b/s模型:
browser-serverC/S B/S优点: 缓存大量数据、协议选择灵活 安全性、跨平台、开发工作量较小速度快缺点: 安全性、跨平台、开发工作量较大 不能缓存大量数据、严格遵守 http
网络传输流程:
数据没有封装之前,是不能在网络中传递。数据-》应用层-》传输层-》网络层-》链路层 --- 网络环境
以太网帧协议:
ARP协议:根据 Ip 地址获取 mac 地址。以太网帧协议:根据mac地址,完成数据包传输。
IP协议:
版本: IPv4、IPv6 -- 4位TTL: time to live 。 设置数据包在路由节点中的跳转上限。每经过一个路由节点,该值-1, 减为0的路由,有义务将该数据包丢弃源IP: 32位。--- 4字节 192.168.1.108 --- 点分十进制 IP地址(string) --- 二进制 目的IP:32位。--- 4字节
IP地址:可以在网络环境中,唯一标识一台主机。
端口号:可以网络的一台主机上,唯一标识一个进程。
ip地址+端口号:可以在网络环境中,唯一标识一个进程。
UDP:
16位:源端口号。 2^16 = 65536
16位:目的端口号。
TCP协议:
16位:源端口号。 2^16 = 65536 16位:目的端口号。32序号;32确认序号。 6个标志位。16位窗口大小。 2^16 = 65536
网络套接字: socket
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)在通信过程中, 套接字一定是成对出现的。
网络字节序:
小端法:(pc本地存储) 高位存高地址。地位存低地址。 int a = 0x12345678大端法:(网络存储) 高位存低地址。地位存高地址。htonl --> 本地--》网络 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序htons --> 本地--》网络 (port)ntohl --> 网络--》 本地(IP)ntohs --> 网络--》 本地(Port)
IP地址转换函数:
int inet_pton(int af, const char *src, void *dst); 本地字节序(string IP) ---> 网络字节序af:AF_INET、AF_INET6src:传入,IP地址(点分十进制)dst:传出,转换后的 网络字节序的 IP地址。 返回值:成功: 1异常: 0, 说明src指向的不是一个有效的ip地址。失败:-1const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序 ---> 本地字节序(string IP)af:AF_INET、AF_INET6src: 网络字节序IP地址dst:本地字节序(string IP)size: dst 的大小。返回值: 成功:dst。 失败:NULL
sockaddr地址结构: IP + port –> 在网络环境中唯一标识一个进程。
struct sockaddr_in addr;addr.sin_family = AF_INET/AF_INET6 man 7 ipaddr.sin_port = htons(9527);int dst;inet_pton(AF_INET, "192.157.22.45", (void *)&dst);addr.sin_addr.s_addr = dst;【*】addr.sin_addr.s_addr = htonl(INADDR_ANY); 取出系统中有效的任意IP地址。二进制类型。bind(fd, (struct sockaddr *)&addr, size);
socket函数:
#include <sys/socket.h>int socket(int domain, int type, int protocol); 创建一个 套接字domain:AF_INET、AF_INET6、AF_UNIXtype:SOCK_STREAM、SOCK_DGRAMprotocol: 0 返回值:成功: 新套接字所对应文件描述符失败: -1 errno#include <arpa/inet.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个 地址结构 (IP+port)sockfd: socket 函数返回值struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(8888);addr.sin_addr.s_addr = htonl(INADDR_ANY);addr: 传入参数(struct sockaddr *)&addraddrlen: sizeof(addr) 地址结构的大小。返回值:成功:0失败:-1 errnoint listen(int sockfd, int backlog); 设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)sockfd: socket 函数返回值backlog:上限数值。最大值 128.返回值:成功:0失败:-1 errno int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符。sockfd: socket 函数返回值addr:传出参数。成功与服务器建立连接的那个客户端的地址结构(IP+port)socklen_t clit_addr_len = sizeof(addr);addrlen:传入传出。 &clit_addr_len入:addr的大小。 出:客户端addr实际大小。返回值:成功:能与客户端进行数据通信的 socket 对应的文件描述。失败: -1 , errnoint connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 使用现有的 socket 与服务器建立连接sockfd: socket 函数返回值struct sockaddr_in srv_addr; // 服务器地址结构srv_addr.sin_family = AF_INET;srv_addr.sin_port = 9527 跟服务器bind时设定的 port 完全一致。inet_pton(AF_INET, "服务器的IP地址",&srv_adrr.sin_addr.s_addr);addr:传入参数。服务器的地址结构addrlen:服务器的地址结构的大小返回值:成功:0失败:-1 errno如果不使用bind绑定客户端地址结构, 采用"隐式绑定".
TCP通信流程分析:
server:1. socket() 创建socket2. bind() 绑定服务器地址结构3. listen() 设置监听上限4. accept() 阻塞监听客户端连接5. read(fd) 读socket获取客户端数据6. 小--大写 toupper()7. write(fd)8. close();client:1. socket() 创建socket2. connect(); 与服务器建立连接3. write() 写数据到 socket4. read() 读转换后的数据。5. 显示读取结果6. close()
25P-复习
26P-三次握手建立连接
27P-数据通信
并不是一次发送,一次应答。也可以批量应答
28P-四次握手关闭连接
29P-半关闭补充说明
这里其实就是想说明,完成两次挥手后,不是说两端的连接断开了,主动端关闭了写缓冲区,不能再向对端发送数据,被动端关闭了读缓冲区,不能再从对端读取数据。然而主动端还是能够读取对端发来的数据。
30P-滑动窗口和TCP数据包格式
滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
31P-通信时序与代码对应关系
32P-TCP通信时序总结
三次握手:
主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带确认序号。
四次挥手:
主动关闭连接请求端, 发送 FIN 标志位。 被动关闭连接请求端, 应答 ACK 标志位。 ----- 半关闭完成。被动关闭连接请求端, 发送 FIN 标志位。主动关闭连接请求端, 应答 ACK 标志位。 ----- 连接全部关闭
滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
33P-错误处理函数的封装思路
wrap.h文件如下,就是包裹函数的声明
- #ifndef _WRAP_H
- #define _WRAP_H
- void perr_exit(const char *s);
- int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
- int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
- int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
- int Listen(int fd, int backlog);
- int Socket(int family, int type, int protocol);
- ssize_t Read(int fd, void *ptr, size_t nbytes);
- ssize_t Write(int fd, const void *ptr, size_t nbytes);
- int Close(int fd);
- ssize_t Readn(int fd, void *vptr, size_t n);
- ssize_t Writen(int fd, const void *vptr, size_t n);
- ssize_t my_read(int fd, char *ptr);
- ssize_t Readline(int fd, void *vptr, size_t maxlen);
- #endif
wrap.c随便取一部分,如下,就是包裹函数的代码:
- #include <stdlib.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <errno.h>
- #include <sys/socket.h>
- void perr_exit(const char *s)
- {
-
perror(s);
-
exit(-1);
- }
- int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
- {
-
int n;
- again:
-
if ((n = accept(fd, sa, salenptr)) < 0) {
-
if ((errno == ECONNABORTED) || (errno == EINTR))
-
goto again;
-
else
-
perr_exit("accept error");
-
}
-
return n;
- }
- int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
- {
-
int n;
-
if ((n = bind(fd, sa, salen)) < 0)
-
perr_exit("bind error");
-
return n;
- }
这里原函数和包裹函数的函数名差异只有首字母大写,这是因为man page对字母大小写不敏感,同名的包裹函数一样可以跳转至man page
34P-错误处理函数封装
就是重新包裹需要检查返回值的函数,让代码不那么肥胖。
35P-封装思想总结和readn、readline封装思想说明
错误处理函数:
封装目的: 在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。【wrap.c】 【wrap.h】存放网络通信相关常用 自定义函数 存放 网络通信相关常用 自定义函数原型(声明)。命名方式:系统调用函数首字符大写, 方便查看man手册如:Listen()、Accept();函数功能:调用系统调用函数,处理出错场景。在 server.c 和 client.c 中调用 自定义函数联合编译 server.c 和 wrap.c 生成 serverclient.c 和 wrap.c 生成 client
readn:
读 N 个字节
readline:
读一行
36P-中午复习
三次握手:
主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带确认序号。
四次挥手:
主动关闭连接请求端, 发送 FIN 标志位。 被动关闭连接请求端, 应答 ACK 标志位。 ----- 半关闭完成。被动关闭连接请求端, 发送 FIN 标志位。主动关闭连接请求端, 应答 ACK 标志位。 ----- 连接全部关闭
滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
错误处理函数:
封装目的: 在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。【wrap.c】 【wrap.h】存放网络通信相关常用 自定义函数 存放 网络通信相关常用 自定义函数原型(声明)。命名方式:系统调用函数首字符大写, 方便查看man手册如:Listen()、Accept();函数功能:调用系统调用函数,处理出错场景。在 server.c 和 client.c 中调用 自定义函数联合编译 server.c 和 wrap.c 生成 serverclient.c 和 wrap.c 生成 client
readn:
读 N 个字节
readline:
读一行
read 函数的返回值:
1. > 0 实际读到的字节数2. = 0 已经读到结尾(对端已经关闭)【 !重 !点 !】3. -1 应进一步判断errno的值:errno = EAGAIN or EWOULDBLOCK: 设置了非阻塞方式 读。 没有数据到达。 errno = EINTR 慢速系统调用被 中断。errno = “其他情况” 异常。
37P-多进程并发服务器思路分析
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1) {
cfd = Accpet(); 接收客户端连接请求。pid = fork();if (pid == 0){ 子进程 read(cfd) --- 小-》大 --- write(cfd)close(lfd) 关闭用于建立连接的套接字 lfdread()小--大write()} else if (pid > 0) { close(cfd); 关闭用于与客户端通信的套接字 cfd contiue;}}5. 子进程:close(lfd)read()小--大write() 父进程:close(cfd);注册信号捕捉函数: SIGCHLD在回调函数中, 完成子进程回收while (waitpid());
38P-多线程并发服务器分析
多线程并发服务器: server.c
1. Socket(); 创建 监听套接字 lfd2. Bind() 绑定地址结构 Strcut scokaddr_in addr;3. Listen(); 4. while (1) { cfd = Accept(lfd, );pthread_create(&tid, NULL, tfn, (void *)cfd);pthread_detach(tid); // pthead_join(tid, void **); 新线程---专用于回收子线程。}5. 子线程:void *tfn(void *arg) {// close(lfd) 不能关闭。 主线程要使用lfdread(cfd)小--大write(cfd)pthread_exit((void *)10); }
39P-多进程并发服务器实现
第一个版本的代码如下:
- #include <stdio.h>
- #include <ctype.h>
- #include <stdlib.h>
- #include <sys/wait.h>
- #include <string.h>
- #include <strings.h>
- #include <unistd.h>
- #include <errno.h>
- #include <signal.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <pthread.h>
- #include “wrap.h”
- #define SRV_PORT 9999
- int main(int argc, char *argv[])
- {
-
int lfd, cfd;
-
pid_t pid;
-
struct sockaddr_in srv_addr, clt_addr;
-
socklen_t clt_addr_len;
-
char buf[BUFSIZ];
-
int ret, i;
-
//memset(&srv_addr, 0, sizeof(srv_addr)); // 将地址结构清零
-
bzero(&srv_addr, sizeof(srv_addr));
-
srv_addr.sin_family = AF_INET;
-
srv_addr.sin_port = htons(SRV_PORT);
-
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
-
lfd = Socket(AF_INET, SOCK_STREAM, 0);
-
Bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
-
Listen(lfd, 128);
-
clt_addr_len = sizeof(clt_addr);
-
while (1) {
-
cfd = Accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
-
pid = fork();
-
if (pid < 0) {
-
perr_exit("fork error");
-
} else if (pid == 0) {
-
close(lfd);
-
break;
-
} else {
-
close(cfd);
-
continue;
-
}
-
}
-
if (pid == 0) {
-
for (;;) {
-
ret = Read(cfd, buf, sizeof(buf));
-
if (ret == 0) {
-
close(cfd);
-
exit(1);
-
}
-
for (i = 0; i < ret; i++)
-
buf[i] = toupper(buf[i]);
-
write(cfd, buf, ret);
-
write(STDOUT_FILENO, buf, ret);
-
}
-
}
-
return 0;
- }
编译运行,结果如下:
这个代码,有问题。我们Ctrl+C终止一个连接进程,会发现,有僵尸进程。
如上图所示,有个僵尸进程。这是因为父进程在阻塞等待,没来得及去回收这个子进程。
所以需要修改代码,增加子进程回收,用信号捕捉来实现。
修改部分如图所示:
完整代码如下:
- #include <stdio.h>
- #include <ctype.h>
- #include <stdlib.h>
- #include <sys/wait.h>
- #include <string.h>
- #include <strings.h>
- #include <unistd.h>
- #include <errno.h>
- #include <signal.h>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <pthread.h>
- #include “wrap.h”
- #define SRV_PORT 9999
- void catch_child(int signum)
- {
-
while ((waitpid(0, NULL, WNOHANG)) > 0);
-
return ;
- }
- int main(int argc, char *argv[])
- {
-
int lfd, cfd;
-
pid_t pid;
-
struct sockaddr_in srv_addr, clt_addr;
-
socklen_t clt_addr_len;
-
char buf[BUFSIZ];
-
int ret, i;
-
//memset(&srv_addr, 0, sizeof(srv_addr)); // 将地址结构清零
-
bzero(&srv_addr, sizeof(srv_addr));
-
srv_addr.sin_family = AF_INET;
-
srv_addr.sin_port = htons(SRV_PORT);
-
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
-
lfd = Socket(AF_INET, SOCK_STREAM, 0);
-
Bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
-
Listen(lfd, 128);
-
clt_addr_len = sizeof(clt_addr);
-
while (1) {
-
cfd = Accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
-
pid = fork();
-
if (pid < 0) {
-
perr_exit("fork error");
-
} else if (pid == 0) {
-
close(lfd);
-
break;
-
} else {
-
struct sigaction act;
-
act.sa_handler = catch_child;
-
sigemptyset(&act.sa_mask);
-
act.sa_flags = 0;
-
ret = sigaction(SIGCHLD, &act, NULL);
-
if (ret != 0) {
-
perr_exit("sigaction error");
-
}
-
close(cfd);
-
continue;
-
}
-
}
-
if (pid == 0) {
-
for (;;) {
-
ret = Read(cfd, buf, sizeof(buf));
-
if (ret == 0) {
-
close(cfd);
-
exit(1);
-
}
-
for (i = 0; i < ret; i++)
-
buf[i] = toupper(buf[i]);
-
write(cfd, buf, ret);
-
write(STDOUT_FILENO, buf, ret);
-
}
-
}
-
return 0;
- }
这样,当子进程退出时,父进程收到信号,就会去回收子进程了,不会出现僵尸进程。
40P-多进程服务器测试IP地址调整
使用桥接模式,让自己主机和其他人主机处于同一个网段
41P-服务器程序上传外网服务器并访问
scp -r 命令,将本地文件拷贝至远程服务器上目标位置
scp -r 源地址 目标地址
42P-多线程服务器代码review
代码如下:
- #include <stdio.h>
- #include <string.h>
- #include <arpa/inet.h>
- #include <pthread.h>
- #include <ctype.h>
- #include <unistd.h>
- #include <fcntl.h>
- #include “wrap.h”
- #define MAXLINE 8192
- #define SERV_PORT 8000
- struct s_info { //定义一个结构体, 将地址结构跟cfd捆绑
-
struct sockaddr_in cliaddr;
-
int connfd;
- };
- void *do_work(void *arg)
- {
-
int n,i;
-
struct s_info *ts = (struct s_info*)arg;
-
char buf[MAXLINE];
-
char str[INET_ADDRSTRLEN]; //#define INET_ADDRSTRLEN 16 可用"[+d"查看
-
while (1) {
-
n = Read(ts->connfd, buf, MAXLINE); //读客户端
-
if (n == 0) {
-
printf("the client %d closed...n", ts->connfd);
-
break; //跳出循环,关闭cfd
-
}
-
printf("received from %s at PORT %dn",
-
inet_ntop(AF_INET, &(*ts).cliaddr.sin_addr, str, sizeof(str)),
-
ntohs((*ts).cliaddr.sin_port)); //打印客户端信息(IP/PORT)
-
for (i = 0; i < n; i++)
-
buf[i] = toupper(buf[i]); //小写-->大写
-
Write(STDOUT_FILENO, buf, n); //写出至屏幕
-
Write(ts->connfd, buf, n); //回写给客户端
-
}
-
Close(ts->connfd);
-
return (void *)0;
- }
- int main(void)
- {
-
struct sockaddr_in servaddr, cliaddr;
-
socklen_t cliaddr_len;
-
int listenfd, connfd;
-
pthread_t tid;
-
struct s_info ts[256]; //创建结构体数组.
-
int i = 0;
-
listenfd = Socket(AF_INET, SOCK_STREAM, 0); //创建一个socket, 得到lfd
-
bzero(&servaddr, sizeof(servaddr)); //地址结构清零
-
servaddr.sin_family = AF_INET;
-
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //指定本地任意IP
-
servaddr.sin_port = htons(SERV_PORT); //指定端口号
-
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //绑定
-
Listen(listenfd, 128); //设置同一时刻链接服务器上限数
-
printf("Accepting client connect ...n");
-
while (1) {
-
cliaddr_len = sizeof(cliaddr);
-
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); //阻塞监听客户端链接请求
-
ts[i].cliaddr = cliaddr;
-
ts[i].connfd = connfd;
-
pthread_create(&tid, NULL, do_work, (void*)&ts[i]);
-
pthread_detach(tid); //子线程分离,防止僵线程产生.
-
i++;
-
}
-
return 0;
- }
编译运行,结果如下:
43P-read返回值和总结
三次握手:
主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带确认序号。
四次挥手:
主动关闭连接请求端, 发送 FIN 标志位。 被动关闭连接请求端, 应答 ACK 标志位。 ----- 半关闭完成。被动关闭连接请求端, 发送 FIN 标志位。主动关闭连接请求端, 应答 ACK 标志位。 ----- 连接全部关闭
滑动窗口:
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。
错误处理函数:
封装目的: 在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。【wrap.c】 【wrap.h】存放网络通信相关常用 自定义函数 存放 网络通信相关常用 自定义函数原型(声明)。命名方式:系统调用函数首字符大写, 方便查看man手册如:Listen()、Accept();函数功能:调用系统调用函数,处理出错场景。在 server.c 和 client.c 中调用 自定义函数联合编译 server.c 和 wrap.c 生成 serverclient.c 和 wrap.c 生成 client
readn:
读 N 个字节
readline:
读一行
read 函数的返回值:
1. > 0 实际读到的字节数2. = 0 已经读到结尾(对端已经关闭)【 !重 !点 !】3. -1 应进一步判断errno的值:errno = EAGAIN or EWOULDBLOCK: 设置了非阻塞方式 读。 没有数据到达。 errno = EINTR 慢速系统调用被 中断。errno = “其他情况” 异常。
多进程并发服务器:server.c
1. Socket(); 创建 监听套接字 lfd
2. Bind() 绑定地址结构 Strcut scokaddr_in addr;
3. Listen();
4. while (1) {cfd = Accpet(); 接收客户端连接请求。pid = fork();if (pid == 0){ 子进程 read(cfd) --- 小-》大 --- write(cfd)close(lfd) 关闭用于建立连接的套接字 lfdread()小--大write()} else if (pid > 0) { close(cfd); 关闭用于与客户端通信的套接字 cfd contiue;}}5. 子进程:close(lfd)read()小--大write() 父进程:close(cfd);注册信号捕捉函数: SIGCHLD在回调函数中, 完成子进程回收while (waitpid());
多线程并发服务器: server.c
1. Socket(); 创建 监听套接字 lfd2. Bind() 绑定地址结构 Strcut scokaddr_in addr;3. Listen(); 4. while (1) { cfd = Accept(lfd, );pthread_create(&tid, NULL, tfn, (void *)cfd);pthread_detach(tid); // pthead_join(tid, void **); 新线程---专用于回收子线程。}5. 子线程:void *tfn(void *arg) {// close(lfd) 不能关闭。 主线程要使用lfdread(cfd)小--大write(cfd)pthread_exit((void *)10); }
44P-复习
45P-TCP状态-主动发起连接
46P-TCP状态-主动关闭连接
47P-TCP状态-被动接收连接
48P-TCP状态-被动关闭连接
49P-2MSL时长
50P-TCP状态-其他状态
netstat -apn | grep client 查看客户端网络连接状态
netstat -apn | grep port 查看端口的网络连接状态
TCP状态时序图:
结合三次握手、四次挥手 理解记忆。1. 主动发起连接请求端: CLOSE -- 发送SYN -- SEND_SYN -- 接收 ACK、SYN -- SEND_SYN -- 发送 ACK -- ESTABLISHED(数据通信态)2. 主动关闭连接请求端: ESTABLISHED(数据通信态) -- 发送 FIN -- FIN_WAIT_1 -- 接收ACK -- FIN_WAIT_2(半关闭)-- 接收对端发送 FIN -- FIN_WAIT_2(半关闭)-- 回发ACK -- TIME_WAIT(只有主动关闭连接方,会经历该状态)-- 等 2MSL时长 -- CLOSE 3. 被动接收连接请求端: CLOSE -- LISTEN -- 接收 SYN -- LISTEN -- 发送 ACK、SYN -- SYN_RCVD -- 接收ACK -- ESTABLISHED(数据通信态)4. 被动关闭连接请求端: ESTABLISHED(数据通信态) -- 接收 FIN -- ESTABLISHED(数据通信态) -- 发送ACK -- CLOSE_WAIT (说明对端【主动关闭连接端】处于半关闭状态) -- 发送FIN -- LAST_ACK -- 接收ACK -- CLOSE重点记忆: ESTABLISHED、FIN_WAIT_2 <--> CLOSE_WAIT、TIME_WAIT(2MSL)netstat -apn | grep 端口号
2MSL时长:
一定出现在【主动关闭连接请求端】。 --- 对应 TIME_WAIT 状态。保证,最后一个 ACK 能成功被对端接收。(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)
51P-端口复用函数
52P-半关闭及shutdown函数
端口复用:
int opt = 1; // 设置端口复用。setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
半关闭:
通信双方中,只有一端关闭通信。 --- FIN_WAIT_2close(cfd);shutdown(int fd, int how); how: SHUT_RD 关读端SHUT_WR 关写端SHUT_RDWR 关读写shutdown在关闭多个文件描述符应用的文件时,采用全关闭方法。close,只关闭一个。
53P-多路IO转接服务器设计思路
54P-select函数参数简介
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1readfds: 读 文件描述符监听集合。 传入、传出参数writefds:写 文件描述符监听集合。 传入、传出参数 NULLexceptfds:异常 文件描述符监听集合 传入、传出参数 NULLtimeout: > 0: 设置监听超时时长。NULL: 阻塞监听0: 非阻塞监听,轮询返回值:> 0: 所有监听集合(3个)中, 满足对应事件的总数。0: 没有满足监听条件的文件描述符-1: errno
55P-中午复习
56P-select函数原型分析
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds:监听的所有文件描述符中,最大文件描述符+1readfds: 读 文件描述符监听集合。 传入、传出参数writefds:写 文件描述符监听集合。 传入、传出参数 NULLexceptfds:异常 文件描述符监听集合 传入、传出参数 NULLtimeout: > 0: 设置监听超时时长。NULL: 阻塞监听0: 非阻塞监听,轮询返回值:> 0: 所有监听集合(3个)中, 满足对应事件的总数。0: 没有满足监听条件的文件描述符-1: errno
57P-select相关函数参数分析
void FD_CLR(int fd, fd_set *set) 把某一个fd清除出去
int FD_ISSET(int fd, fd_set *set) 判定某个fd是否在位图中
void FD_SET(int fd, fd_set *set) 把某一个fd添加到位图
void FD_ZERO(fd_set *set) 位图所有二进制位置零
select多路IO转接:
原理: 借助内核, select 来监听, 客户端连接、数据通信事件。void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。fd_set rset;FD_ZERO(&rset);void FD_SET(int fd, fd_set *set); --- 将待监听的文件描述符,添加到监听集合中FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。FD_CLR(4, &rset);int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。返回值: 在:1;不在:0;FD_ISSET(4, &rset);
58P-select实现多路IO转接设计思路
思路分析:
int maxfd = 0;lfd = socket() ; 创建套接字maxfd = lfd;bind(); 绑定地址结构listen(); 设置监听上限fd_set rset, allset; 创建r监听集合FD_ZERO(&allset); 将r监听集合清空FD_SET(lfd, &allset); 将 lfd 添加至读集合中。while(1) {rset = allset; 保存监听集合ret = select(lfd+1, &rset, NULL, NULL, NULL); 监听文件描述符集合对应事件。if(ret > 0) { 有监听的描述符满足对应事件if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。cfd = accept(); 建立连接,返回用于通信的文件描述符maxfd = cfd;FD_SET(cfd, &allset); 添加到监听通信描述符集合中。}for (i = lfd+1; i <= 最大文件描述符; i++){FD_ISSET(i, &rset) 有read、write事件read()小 -- 大write();} }
}
59P-select实现多路IO转接-代码review
代码如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <string.h>
- #include <arpa/inet.h>
- #include <ctype.h>
- #include “wrap.h”
- #define SERV_PORT 6666
- int main(int argc, char *argv[])
- {
-
int i, j, n, nready;
-
int maxfd = 0;
-
int listenfd, connfd;
-
char buf[BUFSIZ]; /* #define INET_ADDRSTRLEN 16 */
-
struct sockaddr_in clie_addr, serv_addr;
-
socklen_t clie_addr_len;
-
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
-
int opt = 1;
-
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
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(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
-
Listen(listenfd, 128);
-
fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
-
maxfd = listenfd;
-
FD_ZERO(&allset);
-
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
-
while (1) {
-
rset = allset; /* 每次循环时都从新设置select监控信号集 */
-
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
-
if (nready < 0)
-
perr_exit("select error");
-
if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */
-
clie_addr_len = sizeof(clie_addr);
-
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
-
FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
-
if (maxfd < connfd)
-
maxfd = connfd;
-
if (0 == --nready) /* 只有listenfd有事件, 后续的 for 不需执行 */
-
continue;
-
}
-
for (i = listenfd+1; i <= maxfd; i++) { /* 检测哪个clients 有数据就绪 */
-
if (FD_ISSET(i, &rset)) {
-
if ((n = Read(i, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */
-
Close(i);
-
FD_CLR(i, &allset); /* 解除select对此文件描述符的监控 */
-
} else if (n > 0) {
-
for (j = 0; j < n; j++)
-
buf[j] = toupper(buf[j]);
-
Write(i, buf, n);
-
}
-
}
-
}
-
}
-
Close(listenfd);
-
return 0;
- }
编译运行,结果如下:
如图,借助select也可以实现多线程
60P-select实现多路IO转接-代码实现
61P-select实现多路IO转接-添加注释
代码太长了,直接看59话吧
62P-select优缺点
select优缺点:
缺点: 监听上限受文件描述符限制。 最大 1024.检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。优点: 跨平台。win、linux、macOS、Unix、类Unix、mips
select代码里有个可以优化的地方,用数组存下文件描述符,这样就不需要每次扫描一大堆无关文件描述符了
63P-添加一个自定义数组提高效率
这里就是改进之前代码的问题,之前的代码,如果最大fd是1023,每次确定有事件发生的fd时,就要扫描3-1023的所有文件描述符,这看起来很蠢。于是定义一个数组,把要监听的文件描述符存下来,每次扫描这个数组就行了。看起来科学得多。
如图,加个client数组,存要监听的描述符。
代码如下,挺长的
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <string.h>
- #include <arpa/inet.h>
- #include <ctype.h>
- #include “wrap.h”
- #define SERV_PORT 6666
- int main(int argc, char *argv[])
- {
-
int i, j, n, maxi;
-
int nready, client[FD_SETSIZE]; /* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */
-
int maxfd, listenfd, connfd, sockfd;
-
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
-
struct sockaddr_in clie_addr, serv_addr;
-
socklen_t clie_addr_len;
-
fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
-
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
-
int opt = 1;
-
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
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(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
-
Listen(listenfd, 128);
-
maxfd = listenfd; /* 起初 listenfd 即为最大文件描述符 */
-
maxi = -1; /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
-
for (i = 0; i < FD_SETSIZE; i++)
-
client[i] = -1; /* 用-1初始化client[] */
-
FD_ZERO(&allset);
-
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
-
while (1) {
-
rset = allset; /* 每次循环时都重新设置select监控信号集 */
-
nready = select(maxfd+1, &rset, NULL, NULL, NULL); //2 1--lfd 1--connfd
-
if (nready < 0)
-
perr_exit("select error");
-
if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */
-
clie_addr_len = sizeof(clie_addr);
-
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
-
printf("received from %s at PORT %dn",
-
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
-
ntohs(clie_addr.sin_port));
-
for (i = 0; i < FD_SETSIZE; i++)
-
if (client[i] < 0) { /* 找client[]中没有使用的位置 */
-
client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
-
break;
-
}
-
if (i == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */
-
fputs("too many clientsn", stderr);
-
exit(1);
-
}
-
FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
-
if (connfd > maxfd)
-
maxfd = connfd; /* select第一个参数需要 */
-
if (i > maxi)
-
maxi = i; /* 保证maxi存的总是client[]最后一个元素下标 */
-
if (--nready == 0)
-
continue;
-
}
-
for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */
-
if ((sockfd = client[i]) < 0)
-
continue;
-
if (FD_ISSET(sockfd, &rset)) {
-
if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */
-
Close(sockfd);
-
FD_CLR(sockfd, &allset); /* 解除select对此文件描述符的监控 */
-
client[i] = -1;
-
} else if (n > 0) {
-
for (j = 0; j < n; j++)
-
buf[j] = toupper(buf[j]);
-
Write(sockfd, buf, n);
-
Write(STDOUT_FILENO, buf, n);
-
}
-
if (--nready == 0)
-
break; /* 跳出for, 但还在while中 */
-
}
-
}
-
}
-
Close(listenfd);
-
return 0;
- }
编译运行和改进前没啥区别,这里就不贴图了
64P-总结
TCP状态时序图:
结合三次握手、四次挥手 理解记忆。1. 主动发起连接请求端: CLOSE -- 发送SYN -- SEND_SYN -- 接收 ACK、SYN -- SEND_SYN -- 发送 ACK -- ESTABLISHED(数据通信态)2. 主动关闭连接请求端: ESTABLISHED(数据通信态) -- 发送 FIN -- FIN_WAIT_1 -- 接收ACK -- FIN_WAIT_2(半关闭)-- 接收对端发送 FIN -- FIN_WAIT_2(半关闭)-- 回发ACK -- TIME_WAIT(只有主动关闭连接方,会经历该状态)-- 等 2MSL时长 -- CLOSE 3. 被动接收连接请求端: CLOSE -- LISTEN -- 接收 SYN -- LISTEN -- 发送 ACK、SYN -- SYN_RCVD -- 接收ACK -- ESTABLISHED(数据通信态)4. 被动关闭连接请求端: ESTABLISHED(数据通信态) -- 接收 FIN -- ESTABLISHED(数据通信态) -- 发送ACK -- CLOSE_WAIT (说明对端【主动关闭连接端】处于半关闭状态) -- 发送FIN -- LAST_ACK -- 接收ACK -- CLOSE重点记忆: ESTABLISHED、FIN_WAIT_2 <--> CLOSE_WAIT、TIME_WAIT(2MSL)netstat -apn | grep 端口号
2MSL时长:
一定出现在【主动关闭连接请求端】。 --- 对应 TIME_WAIT 状态。保证,最后一个 ACK 能成功被对端接收。(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)
端口复用:
int opt = 1; // 设置端口复用。setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));
半关闭:
通信双方中,只有一端关闭通信。 --- FIN_WAIT_2close(cfd);shutdown(int fd, int how); how: SHUT_RD 关读端SHUT_WR 关写端SHUT_RDWR 关读写shutdown在关闭多个文件描述符应用的文件时,采用全关闭方法。close,只关闭一个。
select多路IO转接:
原理: 借助内核, select 来监听, 客户端连接、数据通信事件。void FD_ZERO(fd_set *set); --- 清空一个文件描述符集合。fd_set rset;FD_ZERO(&rset);void FD_SET(int fd, fd_set *set); --- 将待监听的文件描述符,添加到监听集合中FD_SET(3, &rset); FD_SET(5, &rset); FD_SET(6, &rset);void FD_CLR(int fd, fd_set *set); --- 将一个文件描述符从监听集合中 移除。FD_CLR(4, &rset);int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。返回值: 在:1;不在:0;FD_ISSET(4, &rset);int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);nfds:监听的所有文件描述符中,最大文件描述符+1readfds: 读 文件描述符监听集合。 传入、传出参数writefds:写 文件描述符监听集合。 传入、传出参数 NULLexceptfds:异常 文件描述符监听集合 传入、传出参数 NULLtimeout: > 0: 设置监听超时时长。NULL: 阻塞监听0: 非阻塞监听,轮询返回值:> 0: 所有监听集合(3个)中, 满足对应事件的总数。0: 没有满足监听条件的文件描述符-1: errno
思路分析:
int maxfd = 0;lfd = socket() ; 创建套接字maxfd = lfd;bind(); 绑定地址结构listen(); 设置监听上限fd_set rset, allset; 创建r监听集合FD_ZERO(&allset); 将r监听集合清空FD_SET(lfd, &allset); 将 lfd 添加至读集合中。while(1) {rset = allset; 保存监听集合ret = select(lfd+1, &rset, NULL, NULL, NULL); 监听文件描述符集合对应事件。if(ret > 0) { 有监听的描述符满足对应事件if (FD_ISSET(lfd, &rset)) { // 1 在。 0不在。cfd = accept(); 建立连接,返回用于通信的文件描述符maxfd = cfd;FD_SET(cfd, &allset); 添加到监听通信描述符集合中。}for (i = lfd+1; i <= 最大文件描述符; i++){FD_ISSET(i, &rset) 有read、write事件read()小 -- 大write();} }
}
select优缺点:
缺点: 监听上限受文件描述符限制。 最大 1024.检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。优点: 跨平台。win、linux、macOS、Unix、类Unix、mips
65P-复习
66P-poll函数原型分析
poll是对select的改进,但是它是个半成品,相对select提升不大。最终版本是epoll,所以poll了解一下就完事儿,重点掌握epoll。
poll:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:监听的文件描述符【数组】struct pollfd {int fd: 待监听的文件描述符short events: 待监听的文件描述符对应的监听事件取值:POLLIN、POLLOUT、POLLERRshort revnets: 传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR}nfds: 监听数组的,实际有效监听个数。timeout: > 0: 超时时长。单位:毫秒。-1: 阻塞等待0: 不阻塞返回值:返回满足对应监听事件的文件描述符 总个数。优点:自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。拓展 监听上限。 超出 1024限制。缺点:不能跨平台。 Linux无法直接定位满足监听事件的文件描述符, 编码难度较大。
67P-poll函数使用注意事项示例
68P-poll函数实现服务器
这个东西用得少,基本都用epoll,从讲义上挂个代码过来,看看视频里思路就完事儿
- /* server.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <poll.h>
- #include <errno.h>
- #include “wrap.h”
- #define MAXLINE 80
- #define SERV_PORT 6666
- #define OPEN_MAX 1024
- int main(int argc, char *argv[])
- {
-
int i, j, maxi, listenfd, connfd, sockfd;
-
int nready;
-
ssize_t n;
-
char buf[MAXLINE], str[INET_ADDRSTRLEN];
-
socklen_t clilen;
-
struct pollfd client[OPEN_MAX];
-
struct sockaddr_in cliaddr, servaddr;
-
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);
-
client[0].fd = listenfd;
-
client[0].events = POLLRDNORM; /* listenfd监听普通读事件 */
-
for (i = 1; i < OPEN_MAX; i++)
-
client[i].fd = -1; /* 用-1初始化client[]里剩下元素 */
-
maxi = 0; /* client[]数组有效元素中最大元素下标 */
-
for ( ; ; ) {
-
nready = poll(client, maxi+1, -1); /* 阻塞 */
-
if (client[0].revents & POLLRDNORM) { /* 有客户端链接请求 */
-
clilen = sizeof(cliaddr);
-
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
-
printf("received from %s at PORT %dn",
-
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
-
ntohs(cliaddr.sin_port));
-
for (i = 1; i < OPEN_MAX; i++) {
-
if (client[i].fd < 0) {
-
client[i].fd = connfd; /* 找到client[]中空闲的位置,存放accept返回的connfd */
-
break;
-
}
-
}
-
if (i == OPEN_MAX)
-
perr_exit("too many clients");
-
client[i].events = POLLRDNORM; /* 设置刚刚返回的connfd,监控读事件 */
-
if (i > maxi)
-
maxi = i; /* 更新client[]中最大元素下标 */
-
if (--nready <= 0)
-
continue; /* 没有更多就绪事件时,继续回到poll阻塞 */
-
}
-
for (i = 1; i <= maxi; i++) { /* 检测client[] */
-
if ((sockfd = client[i].fd) < 0)
-
continue;
-
if (client[i].revents & (POLLRDNORM | POLLERR)) {
-
if ((n = Read(sockfd, buf, MAXLINE)) < 0) {
-
if (errno == ECONNRESET) { /* 当收到 RST标志时 */
-
/* connection reset by client */
-
printf("client[%d] aborted connectionn", i);
-
Close(sockfd);
-
client[i].fd = -1;
-
} else {
-
perr_exit("read error");
-
}
-
} else if (n == 0) {
-
/* connection closed by client */
-
printf("client[%d] closed connectionn", i);
-
Close(sockfd);
-
client[i].fd = -1;
-
} else {
-
for (j = 0; j < n; j++)
-
buf[j] = toupper(buf[j]);
-
Writen(sockfd, buf, n);
-
}
-
if (--nready <= 0)
-
break; /* no more readable descriptors */
-
}
-
}
-
}
-
return 0;
- }
69P-poll总结
优点:
自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。
拓展 监听上限。 超出 1024限制。
缺点:
不能跨平台。 Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。
70P-epoll函数实现的多路IO转接
代码如下:
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <string.h>
- #include <arpa/inet.h>
- #include <sys/epoll.h>
- #include <errno.h>
- #include <ctype.h>
- #include “wrap.h”
- #define MAXLINE 8192
- #define SERV_PORT 8000
- #define OPEN_MAX 5000
- int main(int argc, char *argv[])
- {
-
int i, listenfd, connfd, sockfd;
-
int n, num = 0;
-
ssize_t nready, efd, res;
-
char buf[MAXLINE], str[INET_ADDRSTRLEN];
-
socklen_t clilen;
-
struct sockaddr_in cliaddr, servaddr;
-
struct epoll_event tep, ep[OPEN_MAX]; //tep: epoll_ctl参数 ep[] : epoll_wait参数
-
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
-
int opt = 1;
-
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); //端口复用
-
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);
-
efd = epoll_create(OPEN_MAX); //创建epoll模型, efd指向红黑树根节点
-
if (efd == -1)
-
perr_exit("epoll_create error");
-
tep.events = EPOLLIN;
-
tep.data.fd = listenfd; //指定lfd的监听时间为"读"
-
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep); //将lfd及对应的结构体设置到树上,efd可找到该树
-
if (res == -1)
-
perr_exit("epoll_ctl error");
-
for ( ; ; ) {
-
/*epoll为server阻塞监听事件, ep为struct epoll_event类型数组, OPEN_MAX为数组容量, -1表永久阻塞*/
-
nready = epoll_wait(efd, ep, OPEN_MAX, -1);
-
if (nready == -1)
-
perr_exit("epoll_wait error");
-
for (i = 0; i < nready; i++) {
-
if (!(ep[i].events & EPOLLIN)) //如果不是"读"事件, 继续循环
-
continue;
-
if (ep[i].data.fd == listenfd) { //判断满足事件的fd是不是lfd
-
clilen = sizeof(cliaddr);
-
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen); //接受链接
-
printf("received from %s at PORT %dn",
-
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
-
ntohs(cliaddr.sin_port));
-
printf("cfd %d---client %dn", connfd, ++num);
-
tep.events = EPOLLIN; tep.data.fd = connfd;
-
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep); //加入红黑树
-
if (res == -1)
-
perr_exit("epoll_ctl error");
-
} else { //不是lfd,
-
sockfd = ep[i].data.fd;
-
n = Read(sockfd, buf, MAXLINE);
-
if (n == 0) { //读到0,说明客户端关闭链接
-
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); //将该文件描述符从红黑树摘除
-
if (res == -1)
-
perr_exit("epoll_ctl error");
-
Close(sockfd); //关闭与该客户端的链接
-
printf("client[%d] closed connectionn", sockfd);
-
} else if (n < 0) { //出错
-
perror("read n < 0 error: ");
-
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL); //摘除节点
-
Close(sockfd);
-
} else { //实际读到了字节数
-
for (i = 0; i < n; i++)
-
buf[i] = toupper(buf[i]); //转大写,写回给客户端
-
Write(STDOUT_FILENO, buf, n);
-
Writen(sockfd, buf, n);
-
}
-
}
-
}
-
}
-
Close(listenfd);
-
Close(efd);
-
return 0;
- }
71P-突破1024文件描述符设置
突破 1024 文件描述符限制:
cat /proc/sys/fs/file-max --> 当前计算机所能打开的最大文件个数。 受硬件影响。ulimit -a ——> 当前用户下的进程,默认打开文件描述符个数。 缺省为 1024修改:打开 sudo vi /etc/security/limits.conf, 写入:* soft nofile 65536 --> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】* hard nofile 100000 --> 命令修改上限。
cat /proc/sys/fs/file-max 查看最大文件描述符上限
ulimit -a
sudo vi /etc/security/limits.conf 修改上限
修改之后,注销用户重新登录,查看文件描述符上限:
如图,已经修改成功了。
如果使用ulimit -n 来修改,会受到之前设置的hard的限制:
用ulimit -n设置之后,往下调可以,往上调需要注销用户再登录。
72P-epoll_create和epoll_ctl
epoll:
int epoll_create(int size); 创建一棵监听红黑树
size:创建的红黑树的监听节点数量。(仅供内核参考。)返回值:指向新创建的红黑树的根节点的 fd。 失败: -1 errnoint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作监听红黑树epfd:epoll_create 函数的返回值。 epfdop:对该监听红黑数所做的操作。EPOLL_CTL_ADD 添加fd到 监听红黑树EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)fd:待监听的fdevent: 本质 struct epoll_event 结构体 地址成员 events:EPOLLIN / EPOLLOUT / EPOLLERR成员 data: 联合体(共用体):int fd; 对应监听事件的 fdvoid *ptr; uint32_t u32;uint64_t u64; 返回值:成功 0; 失败: -1 errno
73P-epoll_wait函数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞监听。
epfd:epoll_create 函数的返回值。 epfdevents:传出参数,【数组】, 满足监听条件的 那些 fd 结构体。maxevents:数组 元素的总个数。 1024struct epoll_event evnets[1024]timeout:-1: 阻塞0: 不阻塞>0: 超时时间 (毫秒)返回值:> 0: 满足监听的 总个数。 可以用作循环上限。0: 没有fd满足监听事件-1:失败。 errno
74P-中午复习
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);} 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);}}
}
75P-ET和LT模式
epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
EPOLL事件有两种模型:
Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
Level Triggered (LT) 水平触发只要有数据都会触发。
视频中epoll测试代码如下,用一个子进程来写内容,用ET和LT模式来读取,结果很能说明问题:
- #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) {
-
//aaaan
-
for (i = 0; i < MAXLINE/2; i++)
-
buf[i] = ch;
-
buf[i-1] = 'n';
-
ch++;
-
//bbbbn
-
for (; i < MAXLINE; i++)
-
buf[i] = ch;
-
buf[i-1] = 'n';
-
ch++;
-
//aaaanbbbbn
-
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 %dn", 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;
- }
简单理解就是,水平触发就是有数据就触发,边沿触发是有新数据进来才触发。学电子的就比较清楚,触发器就有这个分类。
76P-网络中ET和LT模式
直接看代码,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 %dn",
-
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 %dn", 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) {
-
//aaaan
-
for (i = 0; i < MAXLINE/2; i++)
-
buf[i] = ch;
-
buf[i-1] = 'n';
-
ch++;
-
//bbbbn
-
for (; i < MAXLINE; i++)
-
buf[i] = ch;
-
buf[i-1] = 'n';
-
ch++;
-
//aaaanbbbbn
-
write(sockfd, buf, sizeof(buf));
-
sleep(5);
-
}
-
close(sockfd);
-
return 0;
- }
server边沿触发,编译运行,结果如下:
运行后,每过5秒钟服务器才输出一组字符,这是就是边沿触发的效果。
更改服务器为水平触发模式,运行程序,如下:
运行后,每5秒输出两组字符串,这是因为只写入了两组,这个模式的服务器,缓冲区有多少读多少。
ET模式:
边沿触发:缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。struct epoll_event event;event.events = EPOLLIN | EPOLLET;
LT模式:水平触发 -- 默认采用模式。缓冲区剩余未读尽的数据会导致 epoll_wait 返回。
77P-epoll的ET非阻塞模式
readn调用的阻塞,比如设定读500个字符,但是只读到498,完事儿阻塞了,等另剩下的2个字符,然而在server代码里,一旦read变为readn阻塞了,它就不会被唤醒了,因为epoll_wait因为readn的阻塞不会循环执行,读不到新数据。有点死锁的意思,差俩字符所以阻塞,因为阻塞,读不到新字符。
LT(level triggered):LT是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET(edge-triggered):ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once).
用fcntl设置阻塞
非阻塞epoll的服务器代码如下:
- #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 %dn",
-
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 beginn");
-
res = epoll_wait(efd, res_event, 10, -1); //最多10个, 阻塞监听
-
printf("epoll_wait end res %dn", res);
-
if (res_event[0].data.fd == connfd) {
-
while ((len = read(connfd, buf, MAXLINE/2)) >0 ) //非阻塞读, 轮询
-
write(STDOUT_FILENO, buf, len);
-
}
-
}
-
return 0;
- }
其实就是多了这几行:
结论:
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。
后面使用epoll就用这种非阻塞的
78P-epoll优缺点总结
优点:
高效。突破1024文件描述符。
缺点:
不能跨平台。 Linux。
79P-补充对比ET和LT
这里重要的就一点,当使用非阻塞读时,读取数据需要轮询。
比如使用readn的时候,数据没读够,因为非阻塞,跑了,想读剩下的,就得轮询。
80P-epoll反应堆模型总述
epoll 反应堆模型:
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 监听
反应堆的理解:加入IO转接之后,有了事件,server才去处理,这里反应堆也是这样,由于网络环境复杂,服务器处理数据之后,可能并不能直接写回去,比如遇到网络繁忙或者对方缓冲区已经满了这种情况,就不能直接写回给客户端。反应堆就是在处理数据之后,监听写事件,能写会客户端了,才去做写回操作。写回之后,再改为监听读事件。如此循环。
81P-epoll反应堆main逻辑
直接上代码,这就略微有点长了:
- /*
- *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, %sn", __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, %sn", __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] = ''; //手动添加字符串结束标记
-
printf("C[%d]:%sn", 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], closedn", fd, ev-g_events);
-
} else {
-
close(ev->fd);
-
printf("recv[fd=%d] error[%d]:%sn", 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]%sn", 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 %sn", 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 %sn", __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] timeoutn", 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, exitn");
-
break;
-
}
-
for (i = 0; i < nfd; i++) {
-
/*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/
-
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
-
if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) { //读就绪事件
-
ev->call_back(ev->fd, events[i].events, ev->arg);
-
//lfd EPOLLIN
-
}
-
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) { //写就绪事件
-
ev->call_back(ev->fd, events[i].events, ev->arg);
-
}
-
}
-
}
-
/* 退出前释放所有资源 */
-
return 0;
- }
main逻辑:创建套接字—》初始化连接—》超时验证—》监听—》处理读事件和写事件
82P-epoll反应堆-给lfd和cfd指定回调函数
eventset函数指定了不同事件对应的回调函数,所以虽然读写事件都用的call_back来回调,但实际上调用的是不同的函数。
83P-epoll反应堆initlistensocket小总结
eventset函数:
设置回调函数。 lfd --》 acceptconn()cfd --> recvdata();cfd --> senddata();
eventadd函数:
将一个fd, 添加到 监听红黑树。 设置监听 read事件,还是监听写事件。
84P-epoll反应堆wait被触发后read和write回调及监听
网络编程中: read — recv()
write --- send();
85P-epoll反应堆-超时时间
用一个last_active存储上次活跃时间,完事儿用当前时间和上次活跃时间来计算不活跃时间长度,不活跃时间超过一定阈值,就踢掉这个客户端。
86-总结
多路IO转接:
select:
poll:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:监听的文件描述符【数组】struct pollfd {int fd: 待监听的文件描述符short events: 待监听的文件描述符对应的监听事件取值:POLLIN、POLLOUT、POLLERRshort revnets: 传入时, 给0。如果满足对应事件的话, 返回 非0 --> POLLIN、POLLOUT、POLLERR}nfds: 监听数组的,实际有效监听个数。timeout: > 0: 超时时长。单位:毫秒。-1: 阻塞等待0: 不阻塞返回值:返回满足对应监听事件的文件描述符 总个数。优点:自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。拓展 监听上限。 超出 1024限制。缺点:不能跨平台。 Linux无法直接定位满足监听事件的文件描述符, 编码难度较大。
read 函数返回值:
> 0: 实际读到的字节数=0: socket中,表示对端关闭。close()-1: 如果 errno == EINTR 被异常终端。 需要重启。如果 errno == EAGIN 或 EWOULDBLOCK 以非阻塞方式读数据,但是没有数据。 需要,再次读。如果 errno == ECONNRESET 说明连接被 重置。 需要 close(),移除监听队列。错误。
突破 1024 文件描述符限制:
cat /proc/sys/fs/file-max --> 当前计算机所能打开的最大文件个数。 受硬件影响。ulimit -a ——> 当前用户下的进程,默认打开文件描述符个数。 缺省为 1024修改:打开 sudo vi /etc/security/limits.conf, 写入:* soft nofile 65536 --> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】* hard nofile 100000 --> 命令修改上限。
epoll:
int epoll_create(int size); 创建一棵监听红黑树
size:创建的红黑树的监听节点数量。(仅供内核参考。)返回值:指向新创建的红黑树的根节点的 fd。 失败: -1 errnoint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作监听红黑树epfd:epoll_create 函数的返回值。 epfdop:对该监听红黑数所做的操作。EPOLL_CTL_ADD 添加fd到 监听红黑树EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)fd:待监听的fdevent: 本质 struct epoll_event 结构体 地址成员 events:EPOLLIN / EPOLLOUT / EPOLLERR成员 data: 联合体(共用体):int fd; 对应监听事件的 fdvoid *ptr; uint32_t u32;uint64_t u64; 返回值:成功 0; 失败: -1 errnoint epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞监听。epfd:epoll_create 函数的返回值。 epfdevents:传出参数,【数组】, 满足监听条件的 哪些 fd 结构体。maxevents:数组 元素的总个数。 1024struct 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);} 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模式:边沿触发:缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。struct epoll_event event;event.events = EPOLLIN | EPOLLET;
LT模式:水平触发 -- 默认采用模式。缓冲区剩余未读尽的数据会导致 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。
epoll 反应堆模型:
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();
87P-复习
88P-补充说明epoll的man手册
89P-epoll反应堆再说明
90P-ctags使用
91P线程池模型原理分析
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; / 上面函数的参数 */
92P-线程池描述结构体
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 */
};
93P-线程池main架构
-
main();
创建线程池。向线程池中添加任务。 借助回调处理任务。销毁线程池。
94P-线程池-pthreadpool_create
2. pthreadpool_create();
创建线程池结构体 指针。初始化线程池结构体 { N 个成员变量 }创建 N 个任务线程。创建 1 个管理者线程。失败时,销毁开辟的所有空间。(释放)
95P-子线程回调函数
3. threadpool_thread()
进入子线程回调函数。接收参数 void *arg --》 pool 结构体加锁 --》lock --》 整个结构体锁判断条件变量 --》 wait -------------------170
96P-管理者线程
4. adjust_thread()
循环 10 s 执行一次。进入管理者线程回调函数接收参数 void *arg --》 pool 结构体加锁 --》lock --》 整个结构体锁获取管理线程池要用的到 变量。 task_num, live_num, busy_num根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。
97P-threadpool_add函数
5. threadpool_add ()
总功能:模拟产生任务。 num[20]设置回调函数, 处理任务。 sleep(1) 代表处理完成。内部实现:加锁初始化 任务队列结构体成员。 回调函数 function, arg利用环形队列机制,实现添加任务。 借助队尾指针挪移 % 实现。唤醒阻塞在 条件变量上的线程。解锁
98P-条件满足,子线程wait被唤醒后处理任务
6. 从 3. 中的wait之后继续执行,处理任务。
加锁获取 任务处理回调函数,及参数利用环形队列机制,实现处理任务。 借助队头指针挪移 % 实现。唤醒阻塞在 条件变量 上的 server。解锁加锁 改忙线程数++解锁执行处理任务的线程加锁 改忙线程数——解锁
99P-线程池扩容和销毁
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();
100P-TCP和UDP通信优缺点
TCP通信和UDP通信各自的优缺点:
TCP: 面向连接的,可靠数据包传输。对于不稳定的网络层,采取完全弥补的通信方式。 丢包重传。优点:稳定。 数据流量稳定、速度稳定、顺序缺点:传输速度慢。相率低。开销大。使用场景:数据的完整型要求较高,不追求效率。大数据传输、文件传输。UDP: 无连接的,不可靠的数据报传递。对于不稳定的网络层,采取完全不弥补的通信方式。 默认还原网络状况优点:传输速度块。相率高。开销小。缺点:不稳定。数据流量。速度。顺序。使用场景:对时效性要求较高场合。稳定性其次。游戏、视频会议、视频电话。 腾讯、华为、阿里 --- 应用层数据校验协议,弥补udp的不足。
101P-UDP通信server和client流程
UDP实现的 C/S 模型:
recv()/send() 只能用于 TCP 通信。 替代 read、writeaccpet(); ---- Connect(); ---被舍弃server:lfd = socket(AF_INET, STREAM, 0); SOCK_DGRAM --- 报式协议。bind();listen(); --- 可有可无while(1){read(cfd, buf, sizeof) --- 被替换 --- recvfrom() --- 涵盖accept传出地址结构。
小– 大
write();--- 被替换 --- sendto()---- connect}close();
client:
connfd = socket(AF_INET, SOCK_DGRAM, 0);sendto(‘服务器的地址结构’, 地址结构大小)recvfrom()写到屏幕close();
102P-recvfrom和sendto函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
sockfd: 套接字buf:缓冲区地址len:缓冲区大小flags: 0src_addr:(struct sockaddr *)&addr 传出。 对端地址结构addrlen:传入传出。返回值: 成功接收数据字节数。 失败:-1 errn。 0: 对端关闭。
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: 0src_addr:(struct sockaddr *)&addr 传入。 目标地址结构addrlen:地址结构长度。返回值:成功写出数据字节数。 失败 -1, errno
103P-UDP实现的并发服务器和客户端
直接上代码,啃,啃就完事儿,这是服务器代码
- #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 %dn",
-
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;
- }
下面是客户端代码:
- #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;
- }
104P-借助TCP的CS模型,改写UDP的CS模型
看懂前面的,问题就不大了。可以再看一下视频复习复习
105P-本地套接字和网络套接字比较
本地套接字:
IPC: pipe、fifo、mmap、信号、本地套(domain)--- CS模型对比网络编程 TCP C/S模型, 注意以下几点:1. int socket(int domain, int type, int protocol); 参数 domain:AF_INET --> AF_UNIX/AF_LOCAL type: SOCK_STREAM/SOCK_DGRAM 都可以。
2. 地址结构: sockaddr_in --> sockaddr_unstruct 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");bind(fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr)); --> bind(fd, (struct sockaddr *)&srv_addr, len); 3. bind()函数调用成功,会创建一个 socket。因此为保证bind成功,通常我们在 bind之前, 可以使用 unlink("srv.socket");4. 客户端不能依赖 “隐式绑定”。并且应该在通信建立过程中,创建且初始化2个地址结构:1) client_addr --> bind()2) server_addr --> connect();
106P-本地套接字通信
服务器代码:
- #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] = ''; /* 确保打印时,没有乱码出现 */
-
printf("client bind filename %sn", 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;
- }
客户端代码:
- #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;
- }
107P-本地套接字和网络套接字实现对比
由于布局原因,直接看课程笔记比较科学。
linux网络编程资料day51-教学资料课堂笔记.txt
108P-总结
直接看课程笔记linux网络编程资料day51-教学资料课堂笔记.txt
109P-复习
110P-libevent简介
libevent库
开源。精简。跨平台(Windows、Linux、maxos、unix)。专注于网络通信。
111P-libevent库的下载和安装
源码包安装: 参考 README、readme
./configure 检查安装环境 生成 makefilemake 生成 .o 和 可执行文件sudo make install 将必要的资源cp置系统指定目录。进入 sample 目录,运行demo验证库安装使用情况。编译使用库的 .c 时,需要加 -levent 选项。库名 libevent.so --> /usr/local/lib 查看的到。
特性:
基于“事件”异步通信模型。— 回调。
这里遇到一个问题:
解决办法:
解决这个问题的博客
完事儿运行测试,结果如下:
112P-libevent封装的框架思想
libevent框架:
1. 创建 event_base (乐高底座)
2. 创建 事件evnet
3. 将事件 添加到 base上
4. 循环监听事件满足
5. 释放 event_base
-
创建 event_base (乐高底座)
struct event_base *event_base_new(void);struct event_base *base = event_base_new();
-
创建 事件evnet
常规事件 event --> event_new(); bufferevent --> bufferevent_socket_new();
-
将事件 添加到 base上
int event_add(struct event *ev, const struct timeval *tv)
-
循环监听事件满足
int event_base_dispatch(struct event_base *base);event_base_dispatch(base);
-
释放 event_base
event_base_free(base);
113P-结合helloworld初识libevent
特性:
基于“事件”异步通信模型。— 回调。
114P-框架相关的不常用函数
查看支持哪些多路IO:
代码如下:
编译运行,结果如下:
115P-创建事件对象
创建事件event:
struct event *ev;struct event *event_new(struct event_base *base,evutil_socket_t fd,short what,event_callback_fn cb; void *arg);base: event_base_new()返回值。fd: 绑定到 event 上的 文件描述符what:对应的事件(r、w、e)EV_READ 一次 读事件EV_WRTIE 一次 写事件EV_PERSIST 持续触发。 结合 event_base_dispatch 函数使用,生效。cb:一旦事件满足监听条件,回调的函数。typedef void (*event_callback_fn)(evutil_socket_t fd, short, void *) arg: 回调的函数的参数。返回值:成功创建的 event
116P-事件event操作
添加事件到 event_base
int event_add(struct event *ev, const struct timeval *tv);ev: event_new() 的返回值。tv:NULL
销毁事件
int event_free(struct event *ev);ev: event_new() 的返回值。
117P-使用fifo的读写
读端的代码如下:
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <string.h>
- #include <fcntl.h>
- #include <event2/event.h>
- // 对操作处理函数
- void read_cb(evutil_socket_t fd, short what, void *arg)
- {
-
// 读管道
-
char buf[1024] = {0};
-
int len = read(fd, buf, sizeof(buf));
-
printf("read event: %s n", what & EV_READ ? "Yes" : "No");
-
printf("data len = %d, buf = %sn", len, buf);
-
sleep(1);
- }
- // 读管道
- int main(int argc, const char* argv[])
- {
-
unlink("myfifo");
-
//创建有名管道
-
mkfifo("myfifo", 0664);
-
// open file
-
//int fd = open("myfifo", O_RDONLY | O_NONBLOCK);
-
int fd = open("myfifo", O_RDONLY);
-
if(fd == -1)
-
{
-
perror("open error");
-
exit(1);
-
}
-
// 创建个event_base
-
struct event_base* base = NULL;
-
base = event_base_new();
-
// 创建事件
-
struct event* ev = NULL;
-
ev = event_new(base, fd, EV_READ | EV_PERSIST, read_cb, NULL);
-
// 添加事件
-
event_add(ev, NULL);
-
// 事件循环
-
event_base_dispatch(base); // while(1) { epoll();}
-
// 释放资源
-
event_free(ev);
-
event_base_free(base);
-
close(fd);
-
return 0;
- }
如代码所示,这个也遵循libevent搭积木的过程
写管道代码如下:
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <string.h>
- #include <fcntl.h>
- #include <event2/event.h>
- // 对操作处理函数
- void write_cb(evutil_socket_t fd, short what, void *arg)
- {
-
// write管道
-
char buf[1024] = {0};
-
static int num = 0;
-
sprintf(buf, "hello,world-%dn", num++);
-
write(fd, buf, strlen(buf)+1);
-
sleep(1);
- }
- // 写管道
- int main(int argc, const char* argv[])
- {
-
// open file
-
//int fd = open("myfifo", O_WRONLY | O_NONBLOCK);
-
int fd = open("myfifo", O_WRONLY);
-
if(fd == -1)
-
{
-
perror("open error");
-
exit(1);
-
}
-
// 写管道
-
struct event_base* base = NULL;
-
base = event_base_new();
-
// 创建事件
-
struct event* ev = NULL;
-
// 检测的写缓冲区是否有空间写
-
//ev = event_new(base, fd, EV_WRITE , write_cb, NULL);
-
ev = event_new(base, fd, EV_WRITE | EV_PERSIST, write_cb, NULL);
-
// 添加事件
-
event_add(ev, NULL);
-
// 事件循环
-
event_base_dispatch(base);
-
// 释放资源
-
event_free(ev);
-
event_base_free(base);
-
close(fd);
-
return 0;
- }
编译运行,结果如下:
118P-使用fifo的读写编码实现
这个基本上就是把前面代码写了一遍,复习一下,问题不大
119P-未决和非未决
未决和非未决:
非未决: 没有资格被处理未决: 有资格被处理,但尚未被处理event_new --> event ---> 非未决 --> event_add --> 未决 --> dispatch() && 监听事件被触发 --> 激活态 --> 执行回调函数 --> 处理态 --> 非未决 event_add && EV_PERSIST --> 未决 --> event_del --> 非未决
120P-中午复习
121P-bufferevent特性
带缓冲区的事件 bufferevent
#include <event2/bufferevent.h> read/write 两个缓冲. 借助 队列.
122P-bufferevent事件对象创建、销毁
创建、销毁bufferevent:
struct bufferevent *ev;struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options);base: event_basefd: 封装到bufferevent内的 fdoptions:BEV_OPT_CLOSE_ON_FREE返回: 成功创建的 bufferevent事件对象。void bufferevent_socket_free(struct bufferevent *ev);
123P-给bufferevent事件对象设置回调
给bufferevent设置回调:
对比event: event_new( fd, callback ); event_add() -- 挂到 event_base 上。bufferevent_socket_new(fd) bufferevent_setcb( callback )void bufferevent_setcb(struct bufferevent * bufev,bufferevent_data_cb readcb,bufferevent_data_cb writecb,bufferevent_event_cb eventcb,void *cbarg );bufev: bufferevent_socket_new() 返回值readcb: 设置 bufferevent 读缓冲,对应回调 read_cb{ bufferevent_read() 读数据 }writecb: 设置 bufferevent 写缓冲,对应回调 write_cb { } -- 给调用者,发送写成功通知。 可以 NULLeventcb: 设置 事件回调。 也可传NULLtypedef void (*bufferevent_event_cb)(struct bufferevent *bev, short events, void *ctx);void event_cb(struct bufferevent *bev, short events, void *ctx){。。。。。}events: BEV_EVENT_CONNECTEDcbarg: 上述回调函数使用的 参数。read 回调函数类型:typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void*ctx);void read_cb(struct bufferevent *bev, void *cbarg ){.....bufferevent_read(); --- read();}bufferevent_read()函数的原型:size_t bufferevent_read(struct bufferevent *bev, void *buf, size_t bufsize);write 回调函数类型:int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);
124P-缓冲区开启和关闭
启动、关闭 bufferevent的 缓冲区:
void bufferevent_enable(struct bufferevent *bufev, short events); 启动 events: EV_READ、EV_WRITE、EV_READ|EV_WRITE默认、write 缓冲是 enable、read 缓冲是 disablebufferevent_enable(evev, EV_READ); -- 开启读缓冲。
125P-客户端和服务器连接和监听
连接客户端:
socket();connect();int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen);bev: bufferevent 事件对象(封装了fd)address、len:等同于 connect() 参2/3
创建监听服务器:
------ socket();bind();listen();accept();struct evconnlistener * listnerstruct evconnlistener *evconnlistener_new_bind ( struct event_base *base,evconnlistener_cb cb, void *ptr, unsigned flags,int backlog,const struct sockaddr *sa,int socklen);base: event_basecb: 回调函数。 一旦被回调,说明在其内部应该与客户端完成, 数据读写操作,进行通信。ptr: 回调函数的参数flags: LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLEbacklog: listen() 2参。 -1 表最大值sa:服务器自己的地址结构体socklen:服务器自己的地址结构体大小。返回值:成功创建的监听器。
释放监听服务器:
void evconnlistener_free(struct evconnlistener *lev);
126P-libevent实现TCP服务器流程
服务器端 libevent 创建TCP连接:
-
创建event_base
-
创建bufferevent事件对象。bufferevent_socket_new();
-
使用bufferevent_setcb() 函数给 bufferevent的 read、write、event 设置回调函数。
-
当监听的 事件满足时,read_cb会被调用, 在其内部 bufferevent_read();读
-
使用 evconnlistener_new_bind 创建监听服务器, 设置其回调函数,当有客户端成功连接时,这个回调函数会被调用。
-
封装 listner_cb() 在函数内部。完成与客户端通信。
-
设置读缓冲、写缓冲的 使能状态 enable、disable
-
启动循环 event_base_dispath();
-
释放连接。
127P-libevent实现TCP服务器源码分析
服务器源码如下:
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <string.h>
- #include <event2/event.h>
- #include <event2/listener.h>
- #include <event2/bufferevent.h>
- // 读缓冲区回调
- void read_cb(struct bufferevent *bev, void *arg)
- {
-
char buf[1024] = {0};
-
bufferevent_read(bev, buf, sizeof(buf));
-
printf("client say: %sn", buf);
-
char *p = "我是服务器, 已经成功收到你发送的数据!";
-
// 发数据给客户端
-
bufferevent_write(bev, p, strlen(p)+1);
-
sleep(1);
- }
- // 写缓冲区回调
- void write_cb(struct bufferevent *bev, void *arg)
- {
-
printf("I'm服务器, 成功写数据给客户端,写缓冲区回调函数被回调...n");
- }
- // 事件
- void event_cb(struct bufferevent *bev, short events, void *arg)
- {
-
if (events & BEV_EVENT_EOF)
-
{
-
printf("connection closedn");
-
}
-
else if(events & BEV_EVENT_ERROR)
-
{
-
printf("some other errorn");
-
}
-
bufferevent_free(bev);
-
printf("buffevent 资源已经被释放...n");
- }
- void cb_listener(
-
struct evconnlistener *listener,
-
evutil_socket_t fd,
-
struct sockaddr *addr,
-
int len, void *ptr)
- {
- printf(“connect new clientn”);
- struct event_base* base = (struct event_base*)ptr;
- // 通信操作
- // 添加新事件
- struct bufferevent *bev;
- bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
- // 给bufferevent缓冲区设置回调
- bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
- bufferevent_enable(bev, EV_READ);
- }
- int main(int argc, const char* argv[])
- {
-
// init server
-
struct sockaddr_in serv;
-
memset(&serv, 0, sizeof(serv));
-
serv.sin_family = AF_INET;
-
serv.sin_port = htons(9876);
-
serv.sin_addr.s_addr = htonl(INADDR_ANY);
-
struct event_base* base;
-
base = event_base_new();
-
// 创建套接字
-
// 绑定
-
// 接收连接请求
-
struct evconnlistener* listener;
-
listener = evconnlistener_new_bind(base, cb_listener, base,
-
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
-
36, (struct sockaddr*)&serv, sizeof(serv));
-
event_base_dispatch(base);
-
evconnlistener_free(listener);
-
event_base_free(base);
-
return 0;
- }
128P-服务器注意事项
bufev_server的代码,锤起来:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <sys/socket.h>
- #include <event2/event.h>
- #include <event2/bufferevent.h>
- #include <event2/listener.h>
- #include <pthread.h>
- void sys_err(const char *str)
- {
-
perror(str);
-
exit(1);
- }
- // 读事件回调
- void read_cb(struct bufferevent *bev, void *arg)
- {
-
char buf[1024] = {0};
-
// 借助读缓冲,从客户端拿数据
-
bufferevent_read(bev, buf, sizeof(buf));
-
printf("clinet write: %sn", buf);
-
// 借助写缓冲,写数据回给客户端
-
bufferevent_write(bev, "abcdefg", 7);
- }
- // 写事件回调
- void write_cb(struct bufferevent *bev, void *arg)
- {
-
printf("-------fwq------has wroten");
- }
- // 其他事件回调
- void event_cb(struct bufferevent *bev, short events, void *ctx)
- {
- }
- // 被回调,说明有客户端成功连接, cfd已经传入该参数内部。 创建bufferevent事件对象
- // 与客户端完成读写操作。
- void listener_cb(struct evconnlistener *listener, evutil_socket_t sock,
-
struct sockaddr *addr, int len, void *ptr)
- {
-
struct event_base *base = (struct event_base *)ptr;
-
// 创建bufferevent 对象
-
struct bufferevent *bev = NULL;
-
bev = bufferevent_socket_new(base, sock, BEV_OPT_CLOSE_ON_FREE);
-
// 给bufferevent 对象 设置回调 read、write、event
-
void bufferevent_setcb(struct bufferevent * bufev,
-
bufferevent_data_cb readcb,
-
bufferevent_data_cb writecb,
-
bufferevent_event_cb eventcb,
-
void *cbarg );
-
// 设置回调函数
-
bufferevent_setcb(bev, read_cb, write_cb, NULL, NULL);
-
// 启动 read 缓冲区的 使能状态
-
bufferevent_enable(bev, EV_READ);
-
return ;
- }
- int main(int argc, char *argv[])
- {
-
// 定义服务器地址结构
-
struct sockaddr_in srv_addr;
-
bzero(&srv_addr, sizeof(srv_addr));
-
srv_addr.sin_family = AF_INET;
-
srv_addr.sin_port = htons(8765);
-
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
-
// 创建event_base
-
struct event_base *base = event_base_new();
-
/*
-
struct evconnlistener *evconnlistener_new_bind (
-
struct event_base *base,
-
evconnlistener_cb cb,
-
void *ptr,
-
unsigned flags,
-
int backlog,
-
const struct sockaddr *sa,
-
int socklen);
-
*/
-
// 创建服务器监听器:
-
struct evconnlistener *listener = NULL;
-
listener = evconnlistener_new_bind(base, listener_cb, (void *)base,
-
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, -1,
-
(struct sockaddr *)&srv_addr, sizeof(srv_addr));
-
// 启动监听循环
-
event_base_dispatch(base);
-
// 销毁event_base
-
evconnlistener_free(listener);
-
event_base_free(base);
-
return 0;
- }
129P-客户端流程简析和回顾
代码走起:
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <string.h>
- #include <event2/bufferevent.h>
- #include <event2/event.h>
- #include <arpa/inet.h>
- void read_cb(struct bufferevent *bev, void *arg)
- {
-
char buf[1024] = {0};
-
bufferevent_read(bev, buf, sizeof(buf));
-
printf("fwq say:%sn", buf);
-
bufferevent_write(bev, buf, strlen(buf)+1);
-
sleep(1);
- }
- void write_cb(struct bufferevent *bev, void *arg)
- {
-
printf("----------我是客户端的写回调函数,没卵用n");
- }
- void event_cb(struct bufferevent *bev, short events, void *arg)
- {
-
if (events & BEV_EVENT_EOF)
-
{
-
printf("connection closedn");
-
}
-
else if(events & BEV_EVENT_ERROR)
-
{
-
printf("some other errorn");
-
}
-
else if(events & BEV_EVENT_CONNECTED)
-
{
-
printf("已经连接服务器...\(^o^)/...n");
-
return;
-
}
-
// 释放资源
-
bufferevent_free(bev);
- }
- // 客户端与用户交互,从终端读取数据写给服务器
- void read_terminal(evutil_socket_t fd, short what, void *arg)
- {
-
// 读数据
-
char buf[1024] = {0};
-
int len = read(fd, buf, sizeof(buf));
-
struct bufferevent* bev = (struct bufferevent*)arg;
-
// 发送数据
-
bufferevent_write(bev, buf, len+1);
- }
- int main(int argc, const char* argv[])
- {
-
struct event_base* base = NULL;
-
base = event_base_new();
-
int fd = socket(AF_INET, SOCK_STREAM, 0);
-
// 通信的fd放到bufferevent中
-
struct bufferevent* bev = NULL;
-
bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
-
// init server info
-
struct sockaddr_in serv;
-
memset(&serv, 0, sizeof(serv));
-
serv.sin_family = AF_INET;
-
serv.sin_port = htons(9876);
-
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr.s_addr);
-
// 连接服务器
-
bufferevent_socket_connect(bev, (struct sockaddr*)&serv, sizeof(serv));
-
// 设置回调
-
bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
-
// 设置读回调生效
-
// bufferevent_enable(bev, EV_READ);
-
// 创建事件
-
struct event* ev = event_new(base, STDIN_FILENO, EV_READ | EV_PERSIST,
-
read_terminal, bev);
-
// 添加事件
-
event_add(ev, NULL);
-
event_base_dispatch(base);
-
event_free(ev);
-
event_base_free(base);
-
return 0;
- }
130P-总结
linux网络编程资料day61-教学资料课堂笔记.txt
131P-复习
132P-web大练习的概述
写一个供用户访问主机文件的web服务器
133P-HTML文本和标题
134P-HTML文本和标题
和上一话重复,僵硬,跳过
135P-错误页面html
代码比较简单
136P-列表、图片和超链接
137P-http协议请求、应答协议基础格式
138P-服务器框架复习和getline函数
代码如下:
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.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 MAXSIZE 2048
- int init_listen_fd(int port, int epfd)
- {
-
// 创建监听的套接字 lfd
-
int lfd = socket(AF_INET, SOCK_STREAM, 0);
-
if (lfd == -1) {
-
perror("socket error");
-
exit(1);
-
}
-
// 创建服务器地址结构 IP+port
-
struct sockaddr_in srv_addr;
-
bzero(&srv_addr, sizeof(srv_addr));
-
srv_addr.sin_family = AF_INET;
-
srv_addr.sin_port = htons(port);
-
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
-
// 端口复用
-
int opt = 1;
-
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
// 给 lfd 绑定地址结构
-
int ret = bind(lfd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
-
if (ret == -1) {
-
perror("bind error");
-
exit(1);
-
}
-
// 设置监听上限
-
ret = listen(lfd, 128);
-
if (ret == -1) {
-
perror("listen error");
-
exit(1);
-
}
-
// lfd 添加到 epoll 树上
-
struct epoll_event ev;
-
ev.events = EPOLLIN;
-
ev.data.fd = lfd;
-
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
-
if (ret == -1) {
-
perror("epoll_ctl add lfd error");
-
exit(1);
-
}
-
return lfd;
- }
- void do_accept(int lfd, int epfd)
- {
-
struct sockaddr_in clt_addr;
-
socklen_t clt_addr_len = sizeof(clt_addr);
-
int cfd = accept(lfd, (struct sockaddr*)&clt_addr, &clt_addr_len);
-
if (cfd == -1) {
-
perror("accept error");
-
exit(1);
-
}
-
// 打印客户端IP+port
-
char client_ip[64] = {0};
-
printf("New Client IP: %s, Port: %d, cfd = %dn",
-
inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
-
ntohs(clt_addr.sin_port), cfd);
-
// 设置 cfd 非阻塞
-
int flag = fcntl(cfd, F_GETFL);
-
flag |= O_NONBLOCK;
-
fcntl(cfd, F_SETFL, flag);
-
// 将新节点cfd 挂到 epoll 监听树上
-
struct epoll_event ev;
-
ev.data.fd = cfd;
-
// 边沿非阻塞模式
-
ev.events = EPOLLIN | EPOLLET;
-
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
-
if (ret == -1) {
-
perror("epoll_ctl add cfd error");
-
exit(1);
-
}
- }
- void do_read(int cfd, int epfd)
- {
-
// read cfd 小 -- 大 write 回
-
// 读取一行http协议, 拆分, 获取 get 文件名 协议号
- }
- void epoll_run(int port)
- {
-
int i = 0;
-
struct epoll_event all_events[MAXSIZE];
-
// 创建一个epoll监听树根
-
int epfd = epoll_create(MAXSIZE);
-
if (epfd == -1) {
-
perror("epoll_create error");
-
exit(1);
-
}
-
// 创建lfd,并添加至监听树
-
int lfd = init_listen_fd(port, epfd);
-
while (1) {
-
// 监听节点对应事件
-
int ret = epoll_wait(epfd, all_events, MAXSIZE, -1);
-
if (ret == -1) {
-
perror("epoll_wait error");
-
exit(1);
-
}
-
for (i=0; i<ret; ++i) {
-
// 只处理读事件, 其他事件默认不处理
-
struct epoll_event *pev = &all_events[i];
-
// 不是读事件
-
if (!(pev->events & EPOLLIN)) {
-
continue;
-
}
-
if (pev->data.fd == lfd) { // 接受连接请求
-
do_accept(lfd, epfd);
-
} else { // 读数据
-
do_read(pev->data.fd, epfd);
-
}
-
}
-
}
- }
- int main(int argc, char *argv[])
- {
-
// 命令行参数获取 端口 和 server提供的目录
-
if (argc < 3)
-
{
-
printf("./server port pathn");
-
}
-
// 获取用户输入的端口
-
int port = atoi(argv[1]);
-
// 改变进程工作目录
-
int ret = chdir(argv[2]);
-
if (ret != 0) {
-
perror("chdir error");
-
exit(1);
-
}
-
// 启动 epoll监听
-
epoll_run(port);
-
return 0;
- }
139P-复习
请求协议: — 浏览器组织,发送
GET /hello.c Http1.1rn
2. Host: localhost:2222rn
3. User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:24.0) Gecko/201001 01 Firefox/24.0rn
4. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8rn
5. Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3rn
6. Accept-Encoding: gzip, deflatern
7. Connection: keep-alivern
8. If-Modified-Since: Fri, 18 Jul 2014 08:36:36 GMTrn
【空行】rn
应答协议:
Http1.1 200 OK
2. Server: xhttpd
Content-Type:text/plain; charset=iso-8859-1
3. Date: Fri, 18 Jul 2014 14:34:26 GMT
5. Content-Length: 32 ( 要么不写 或者 传-1, 要写务必精确 ! )
6. Content-Language: zh-CN
7. Last-Modified: Fri, 18 Jul 2014 08:36:36 GMT
8. Connection: close
rn
[数据起始。。。。。
。。。。
。。。数据终止]
140P-单文件通信流程分析
-
getline() 获取 http协议的第一行。
-
从首行中拆分 GET、文件名、协议版本。 获取用户请求的文件名。
-
判断文件是否存在。 stat()
-
判断是文件还是目录。
-
是文件– open – read – 写回给浏览器
-
先写 http 应答协议头 : http/1.1 200 ok
Content-Type:text/plain; charset=iso-8859-1
141P-处理出错返回
142P-正则表达式获取文件名
- void do_read(int cfd, int epfd)
- {
-
// 读取一行http协议, 拆分, 获取 get 文件名 协议号
-
char line[1024] = {0};
-
char method[16], path[256], protocol[16];
-
int len = get_line(cfd, line, sizeof(line)); //读 http请求协议首行 GET /hello.c HTTP/1.1
-
if (len == 0) {
-
printf("服务器,检查到客户端关闭....n");
-
disconnect(cfd, epfd);
-
} else {
-
sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
-
printf("method=%s, path=%s, protocol=%sn", method, path, protocol);
-
while (1) {
-
char buf[1024] = {0};
-
len = get_line(cfd, buf, sizeof(buf));
-
if (buf[0] == 'n') {
-
break;
-
} else if (len == -1)
-
break;
-
}
-
}
-
if (strncasecmp(method, "GET", 3) == 0)
-
{
-
char *file = path+1; // 取出 客户端要访问的文件名
-
http_request(cfd, file);
-
disconnect(cfd, epfd);
-
}
- }
143P-判断文件是否存在
- // 处理http请求, 判断文件是否存在, 回发
- void http_request(int cfd, const char *file)
- {
-
struct stat sbuf;
-
// 判断文件是否存在
-
int ret = stat(file, &sbuf);
-
if (ret != 0) {
-
// 回发浏览器 404 错误页面
-
perror("stat");
-
exit(1);
-
}
-
if(S_ISREG(sbuf.st_mode)) { // 是一个普通文件
-
// 回发 http协议应答
-
//send_respond(cfd, 200, "OK", " Content-Type: text/plain; charset=iso-8859-1", sbuf.st_size);
-
send_respond(cfd, 200, "OK", "Content-Type:image/jpeg", -1);
-
//send_respond(cfd, 200, "OK", "audio/mpeg", -1);
-
// 回发 给客户端请求数据内容。
-
send_file(cfd, file);
-
}
- }
144P-写出http应答协议头
-
// 客户端端的fd, 错误号,错误描述,回发文件类型, 文件长度
- void send_respond(int cfd, int no, char *disp, char *type, int len)
- {
-
char buf[4096] = {0};
-
sprintf(buf, "HTTP/1.1 %d %srn", no, disp);
-
send(cfd, buf, strlen(buf), 0);
-
sprintf(buf, "Content-Type: %srn", type);
-
sprintf(buf+strlen(buf), "Content-Length:%drn", len);
-
send(cfd, buf, strlen(buf), 0);
-
send(cfd, "rn", 2, 0);
- }
145P-写数据给浏览器
- // 发送服务器本地文件 给浏览器
- void send_file(int cfd, const char *file)
- {
-
int n = 0, ret;
-
char buf[4096] = {0};
-
// 打开的服务器本地文件。 --- cfd 能访问客户端的 socket
-
int fd = open(file, O_RDONLY);
-
if (fd == -1) {
-
// 404 错误页面
-
perror("open error");
-
exit(1);
-
}
-
while ((n = read(fd, buf, sizeof(buf))) > 0) {
-
ret = send(cfd, buf, n, 0);
-
if (ret == -1) {
-
perror("send error");
-
exit(1);
-
}
-
if (ret < 4096)
-
printf("-----send ret: %dn", ret);
-
}
-
close(fd);
- }
146P-文件类型区分
147P-错误原因及说明
MP3请求错误的原因在于,做错误判断时太粗略,errno=EAGAIN或者errno=EINTR时,并不算错误,此时继续执行循环读取数据就行。
然而原来的程序是直接退出了,所以没接收到数据。
148P-错误页面展示
错误页面部分的代码:
- void send_error(int cfd, int status, char *title, char *text)
- {
-
char buf[4096] = {0};
-
sprintf(buf, "%s %d %srn", "HTTP/1.1", status, title);
-
sprintf(buf+strlen(buf), "Content-Type:%srn", "text/html");
-
sprintf(buf+strlen(buf), "Content-Length:%drn", -1);
-
sprintf(buf+strlen(buf), "Connection: closern");
-
send(cfd, buf, strlen(buf), 0);
-
send(cfd, "rn", 2, 0);
-
memset(buf, 0, sizeof(buf));
-
sprintf(buf, "<html><head><title>%d %s</title></head>n", status, title);
-
sprintf(buf+strlen(buf), "<body bgcolor="#cc99cc"><h2 align="center">%d %s</h4>n", status, title);
-
sprintf(buf+strlen(buf), "%sn", text);
-
sprintf(buf+strlen(buf), "<hr>n</body>n</html>n");
-
send(cfd, buf, strlen(buf), 0);
-
return ;
- }
直接看完整代码吧:
epoll_server.c
149P-关于浏览器请求ico文件
150P-浏览器请求目录
- // http请求处理
- void http_request(const char* request, int cfd)
- {
-
// 拆分http请求行
-
char method[12], path[1024], protocol[12];
-
sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
-
printf("method = %s, path = %s, protocol = %sn", method, path, protocol);
-
// 转码 将不能识别的中文乱码 -> 中文
-
// 解码 %23 %34 %5f
-
decode_str(path, path);
-
char* file = path+1; // 去掉path中的/ 获取访问文件名
-
// 如果没有指定访问的资源, 默认显示资源目录中的内容
-
if(strcmp(path, "/") == 0) {
-
// file的值, 资源目录的当前位置
-
file = "./";
-
}
-
// 获取文件属性
-
struct stat st;
-
int ret = stat(file, &st);
-
if(ret == -1) {
-
send_error(cfd, 404, "Not Found", "NO such file or direntry");
-
return;
-
}
-
// 判断是目录还是文件
-
if(S_ISDIR(st.st_mode)) { // 目录
-
// 发送头信息
-
send_respond_head(cfd, 200, "OK", get_file_type(".html"), -1);
-
// 发送目录信息
-
send_dir(cfd, file);
-
} else if(S_ISREG(st.st_mode)) { // 文件
-
// 发送消息报头
-
send_respond_head(cfd, 200, "OK", get_file_type(file), st.st_size);
-
// 发送文件内容
-
send_file(cfd, file);
-
}
- }
151P-判断文件类型
- // 通过文件名获取文件的类型
- const char *get_file_type(const char *name)
- {
-
char* dot;
-
// 自右向左查找‘.’字符, 如不存在返回NULL
-
dot = strrchr(name, '.');
-
if (dot == NULL)
-
return "text/plain; charset=utf-8";
-
if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
-
return "text/html; charset=utf-8";
-
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
-
return "image/jpeg";
-
if (strcmp(dot, ".gif") == 0)
-
return "image/gif";
-
if (strcmp(dot, ".png") == 0)
-
return "image/png";
-
if (strcmp(dot, ".css") == 0)
-
return "text/css";
-
if (strcmp(dot, ".au") == 0)
-
return "audio/basic";
-
if (strcmp( dot, ".wav" ) == 0)
-
return "audio/wav";
-
if (strcmp(dot, ".avi") == 0)
-
return "video/x-msvideo";
-
if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
-
return "video/quicktime";
-
if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
-
return "video/mpeg";
-
if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
-
return "model/vrml";
-
if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
-
return "audio/midi";
-
if (strcmp(dot, ".mp3") == 0)
-
return "audio/mpeg";
-
if (strcmp(dot, ".ogg") == 0)
-
return "application/ogg";
-
if (strcmp(dot, ".pac") == 0)
-
return "application/x-ns-proxy-autoconfig";
-
return "text/plain; charset=utf-8";
- }
152P-汉字字符编码和解码
URL中的汉字默认是存为Unicode码
153P-libevent实现的web服务器
直接源码啃起来吧
154P-telnet调试