深圳建设银行分行网站,美食网站建设项目预算,创建一个行业网站多少钱,wordpress prevent copy paste01P-复习-Linux网络编程
02P-信号量生产者复习
03P-协议 协议#xff1a; 一组规则。
04P-7层模型和4层模型及代表协议 分层模型结构#xff1a;
OSI七层模型#xff1a; 物、数、网、传、会、表、应TCP/IP 4层模型#xff1a;网#xff08;链路层/网络接口层#x…01P-复习-Linux网络编程
02P-信号量生产者复习
03P-协议 协议 一组规则。
04P-7层模型和4层模型及代表协议 分层模型结构
OSI七层模型 物、数、网、传、会、表、应TCP/IP 4层模型网链路层/网络接口层、网、传、应应用层http、ftp、nfs、ssh、telnet。。。传输层TCP、UDP网络层IP、ICMP、IGMP链路层以太网帧协议、ARP05P-网络传输数据封装流程
网络传输流程
数据没有封装之前是不能在网络中传递。数据-》应用层-》传输层-》网络层-》链路层 --- 网络环境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 --- 二进制 目的IP32位。--- 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 6553610P-BS和CS模型对比 c/s模型
client-serverb/s模型
browser-serverC/S B/S优点 缓存大量数据、协议选择灵活 安全性、跨平台、开发工作量较小速度快缺点 安全性、跨平台、开发工作量较大 不能缓存大量数据、严格遵守 http11P-套接字 网络套接字 socket
一个文件描述符指向一个套接字该套接字内部由内核借助两个缓冲区实现。在通信过程中 套接字一定是成对出现的。12P-回顾
13P-网络字节序 网络字节序
小端法pc本地存储 高位存高地址。地位存低地址。 int a 0x12345678大端法网络存储 高位存低地址。地位存高地址。htonl -- 本地--》网络 IP 192.168.1.11 -- string -- atoi -- int -- htonl -- 网络字节序htons -- 本地--》网络 (port)ntohl -- 网络--》 本地IPntohs -- 网络--》 本地Port14P-IP地址转换函数 IP地址转换函数
int inet_pton(int af, const char *src, void *dst); 本地字节序string IP --- 网络字节序afAF_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 IPafAF_INET、AF_INET6src: 网络字节序IP地址dst本地字节序string IPsize dst 的大小。返回值 成功dst。 失败NULL15P-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.hint socket(int domain, int type, int protocol); 创建一个 套接字domainAF_INET、AF_INET6、AF_UNIXtypeSOCK_STREAM、SOCK_DGRAMprotocol: 0 返回值成功 新套接字所对应文件描述符失败: -1 errno#include arpa/inet.h int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个 地址结构 (IPport)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 errno18P-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传出参数。成功与服务器建立连接的那个客户端的地址结构IPportsocklen_t clit_addr_len sizeof(addr);addrlen传入传出。 clit_addr_len入addr的大小。 出客户端addr实际大小。返回值成功能与客户端进行数据通信的 socket 对应的文件描述。失败 -1 errno19P-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 9527void 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绑定地址结构IPport) 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:%d\n, 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传出的就是客户端地址结构IPport
于是在代码中增加此段代码可获取客户端信息 printf(“client ip:%s port:%d\n”, 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 9527void 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, hello\n, 6); ret read(cfd, buf, sizeof(buf)); write(STDOUT_FILENO, buf, ret); sleep(1); } close(cfd); return 0; }
编译运行结果如下
这里遇到过一个问题如果之前运行server用Ctrlz终止进程ps aux列表里会有服务器进程残留这个会影响当前服务器。解决方法是kill掉这些服务器进程。不然端口被占用当前运行的服务器进程接收不到东西没有回显。24P-总结
协议 一组规则。
分层模型结构
OSI七层模型 物、数、网、传、会、表、应TCP/IP 4层模型网链路层/网络接口层、网、传、应应用层http、ftp、nfs、ssh、telnet。。。传输层TCP、UDP网络层IP、ICMP、IGMP链路层以太网帧协议、ARPc/s模型
client-serverb/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 --- 二进制 目的IP32位。--- 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 -- 网络--》 本地IPntohs -- 网络--》 本地PortIP地址转换函数
int inet_pton(int af, const char *src, void *dst); 本地字节序string IP --- 网络字节序afAF_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 IPafAF_INET、AF_INET6src: 网络字节序IP地址dst本地字节序string IPsize dst 的大小。返回值 成功dst。 失败NULLsockaddr地址结构 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.hint socket(int domain, int type, int protocol); 创建一个 套接字domainAF_INET、AF_INET6、AF_UNIXtypeSOCK_STREAM、SOCK_DGRAMprotocol: 0 返回值成功 新套接字所对应文件描述符失败: -1 errno#include arpa/inet.hint bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 给socket绑定一个 地址结构 (IPport)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传出参数。成功与服务器建立连接的那个客户端的地址结构IPportsocklen_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_Hvoid 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.hvoid 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 生成 clientreadn 读 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 生成 clientreadn 读 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 9999int 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; }
编译运行结果如下
这个代码有问题。我们CtrlC终止一个连接进程会发现有僵尸进程。如上图所示有个僵尸进程。这是因为父进程在阻塞等待没来得及去回收这个子进程。
所以需要修改代码增加子进程回收用信号捕捉来实现。 修改部分如图所示
完整代码如下
#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 9999void 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 8000struct 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 %d\n, 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 生成 clientreadn 读 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_WAIT2MSLnetstat -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_2closecfd;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 errno55P-中午复习
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 errno57P-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_CLR4 rset;int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。返回值 在1不在0FD_ISSET4 rset;58P-select实现多路IO转接设计思路 思路分析
int maxfd 0lfd socket() ; 创建套接字maxfd lfdbind(); 绑定地址结构listen(); 设置监听上限fd_set rset allset; 创建r监听集合FD_ZERO(allset); 将r监听集合清空FD_SET(lfd, allset); 将 lfd 添加至读集合中。while1 {rset allset 保存监听集合ret select(lfd1 rset NULL NULL NULL); 监听文件描述符集合对应事件。ifret 0 { 有监听的描述符满足对应事件if (FD_ISSET(lfd, rset)) { // 1 在。 0不在。cfd accept 建立连接返回用于通信的文件描述符maxfd cfdFD_SET(cfd, allset); 添加到监听通信描述符集合中。}for i lfd1 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 6666int 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(maxfd1, 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 listenfd1; 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、mipsselect代码里有个可以优化的地方用数组存下文件描述符这样就不需要每次扫描一大堆无关文件描述符了
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 6666int 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(maxfd1, 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 %d\n, 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 clients\n, 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_WAIT2MSLnetstat -apn | grep 端口号2MSL时长
一定出现在【主动关闭连接请求端】。 --- 对应 TIME_WAIT 状态。保证最后一个 ACK 能成功被对端接收。等待期间对端没收到我发的ACK对端会再次发送FIN请求。端口复用:
int opt 1; // 设置端口复用。setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, (void *)opt, sizeof(opt));半关闭
通信双方中只有一端关闭通信。 --- FIN_WAIT_2closecfd;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_CLR4 rset;int FD_ISSET(int fd, fd_set *set); --- 判断一个文件描述符是否在监听集合中。返回值 在1不在0FD_ISSET4 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 0lfd socket() ; 创建套接字maxfd lfdbind(); 绑定地址结构listen(); 设置监听上限fd_set rset allset; 创建r监听集合FD_ZERO(allset); 将r监听集合清空FD_SET(lfd, allset); 将 lfd 添加至读集合中。while1 {rset allset 保存监听集合ret select(lfd1 rset NULL NULL NULL); 监听文件描述符集合对应事件。ifret 0 { 有监听的描述符满足对应事件if (FD_ISSET(lfd, rset)) { // 1 在。 0不在。cfd accept 建立连接返回用于通信的文件描述符maxfd cfdFD_SET(cfd, allset); 添加到监听通信描述符集合中。}for i lfd1 i 最大文件描述符; i{FD_ISSET(i, rset) 有read、write事件read小 -- 大write();} }
}select优缺点
缺点 监听上限受文件描述符限制。 最大 1024.检测满足条件的fd 自己添加业务逻辑提高小。 提高了编码难度。优点 跨平台。win、linux、macOS、Unix、类Unix、mips65P-复习
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 1024int 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, maxi1, -1); /* 阻塞 */ if (client[0].revents POLLRDNORM) { /* 有客户端链接请求 */ clilen sizeof(cliaddr); connfd Accept(listenfd, (struct sockaddr *)cliaddr, clilen); printf(received from %s at PORT %d\n, 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 connection\n, i); Close(sockfd); client[i].fd -1; } else { perr_exit(read error); } } else if (n 0) { /* connection closed by client */ printf(client[%d] closed connection\n, 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 5000int 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 %d\n, inet_ntop(AF_INET, cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); printf(cfd %d---client %d\n, 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 connection\n, 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); 操作监听红黑树epfdepoll_create 函数的返回值。 epfdop对该监听红黑数所做的操作。EPOLL_CTL_ADD 添加fd到 监听红黑树EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下取消监听fd待监听的fdevent 本质 struct epoll_event 结构体 地址成员 eventsEPOLLIN / EPOLLOUT / EPOLLERR成员 data 联合体共用体int fd; 对应监听事件的 fdvoid *ptr uint32_t u32;uint64_t u64; 返回值成功 0 失败 -1 errno73P-epoll_wait函数 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞监听。 epfdepoll_create 函数的返回值。 epfdevents传出参数【数组】 满足监听条件的 那些 fd 结构体。maxevents数组 元素的总个数。 1024struct epoll_event evnets[1024]timeout-1: 阻塞0 不阻塞0: 超时时间 毫秒返回值 0: 满足监听的 总个数。 可以用作循环上限。0 没有fd满足监听事件-1失败。 errno74P-中午复习 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 ep1024 -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 10int main(int argc, char *argv[]){ int efd, i; int pfd[2]; pid_t pid; char buf[MAXLINE], ch a; pipe(pfd); pid fork(); if (pid 0) { //子 写 close(pfd[0]); while (1) { //aaaa\n for (i 0; i MAXLINE/2; i) buf[i] ch; buf[i-1] \n; ch; //bbbb\n for (; i MAXLINE; i) buf[i] ch; buf[i-1] \n; ch; //aaaa\nbbbb\n write(pfd[1], buf, sizeof(buf)); sleep(5); } close(pfd[1]); } else if (pid 0) { //父 读 struct epoll_event event; struct epoll_event resevent[10]; //epoll_wait就绪返回event int res, len; close(pfd[1]); efd epoll_create(10); event.events EPOLLIN | EPOLLET; // ET 边沿触发 // event.events EPOLLIN; // LT 水平触发 (默认) event.data.fd pfd[0]; epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], event); while (1) { res epoll_wait(efd, resevent, 10, -1); printf(res %d\n, res); if (resevent[0].data.fd pfd[0]) { len read(pfd[0], buf, MAXLINE/2); write(STDOUT_FILENO, buf, len); } } close(pfd[0]); close(efd); } else { perror(fork); exit(-1); } return 0; }
简单理解就是水平触发就是有数据就触发边沿触发是有新数据进来才触发。学电子的就比较清楚触发器就有这个分类。
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 9000int main(void){ struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int efd; listenfd socket(AF_INET, SOCK_STREAM, 0); bzero(servaddr, sizeof(servaddr)); servaddr.sin_family AF_INET; servaddr.sin_addr.s_addr htonl(INADDR_ANY); servaddr.sin_port htons(SERV_PORT); bind(listenfd, (struct sockaddr *)servaddr, sizeof(servaddr)); listen(listenfd, 20); struct epoll_event event; struct epoll_event resevent[10]; int res, len; efd epoll_create(10); event.events EPOLLIN | EPOLLET; /* ET 边沿触发 */ //event.events EPOLLIN; /* 默认 LT 水平触发 */ printf(Accepting connections ...\n); cliaddr_len sizeof(cliaddr); connfd accept(listenfd, (struct sockaddr *)cliaddr, cliaddr_len); printf(received from %s at PORT %d\n, inet_ntop(AF_INET, cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); event.data.fd connfd; epoll_ctl(efd, EPOLL_CTL_ADD, connfd, event); while (1) { res epoll_wait(efd, resevent, 10, -1); printf(res %d\n, res); if (resevent[0].data.fd connfd) { len read(connfd, buf, MAXLINE/2); //readn(500) write(STDOUT_FILENO, buf, len); } } return 0; }
client代码如下
#include stdio.h#include string.h#include unistd.h#include arpa/inet.h#include netinet/in.h#define MAXLINE 10#define SERV_PORT 9000int main(int argc, char *argv[]){ struct sockaddr_in servaddr; char buf[MAXLINE]; int sockfd, i; char ch a; sockfd socket(AF_INET, SOCK_STREAM, 0); bzero(servaddr, sizeof(servaddr)); servaddr.sin_family AF_INET; inet_pton(AF_INET, 127.0.0.1, servaddr.sin_addr); servaddr.sin_port htons(SERV_PORT); connect(sockfd, (struct sockaddr *)servaddr, sizeof(servaddr)); while (1) { //aaaa\n for (i 0; i MAXLINE/2; i) buf[i] ch; buf[i-1] \n; ch; //bbbb\n for (; i MAXLINE; i) buf[i] ch; buf[i-1] \n; ch; //aaaa\nbbbb\n write(sockfd, buf, sizeof(buf)); sleep(5); } close(sockfd); return 0; }
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 8000int main(void){ struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int efd, flag; listenfd socket(AF_INET, SOCK_STREAM, 0); bzero(servaddr, sizeof(servaddr)); servaddr.sin_family AF_INET; servaddr.sin_addr.s_addr htonl(INADDR_ANY); servaddr.sin_port htons(SERV_PORT); bind(listenfd, (struct sockaddr *)servaddr, sizeof(servaddr)); listen(listenfd, 20); /// struct epoll_event event; struct epoll_event res_event[10]; int res, len; efd epoll_create(10); event.events EPOLLIN | EPOLLET; /* ET 边沿触发默认是水平触发 */ //event.events EPOLLIN; printf(Accepting connections ...\n); cliaddr_len sizeof(cliaddr); connfd accept(listenfd, (struct sockaddr *)cliaddr, cliaddr_len); printf(received from %s at PORT %d\n, inet_ntop(AF_INET, cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); flag fcntl(connfd, F_GETFL); /* 修改connfd为非阻塞读 */ flag | O_NONBLOCK; fcntl(connfd, F_SETFL, flag); event.data.fd connfd; epoll_ctl(efd, EPOLL_CTL_ADD, connfd, event); //将connfd加入监听红黑树 while (1) { printf(epoll_wait begin\n); res epoll_wait(efd, res_event, 10, -1); //最多10个, 阻塞监听 printf(epoll_wait end res %d\n, res); if (res_event[0].data.fd connfd) { while ((len read(connfd, buf, MAXLINE/2)) 0 ) //非阻塞读, 轮询 write(STDOUT_FILENO, buf, len); } } return 0; }
其实就是多了这几行
结论 epoll 的 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 -- while1---- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足 -- read() --- 小-大 -- write回去。反应堆不但要监听 cfd 的读事件、还要监听cfd的写事件。 socket、bind、listen -- epoll_create 创建监听 红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while1---- 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 8080void 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_EVENTS1]; //自定义结构体类型数组. 1–listen fd/将结构体 myevent_s 成员变量 初始化/void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg){ ev-fd fd; ev-call_back call_back; ev-events 0; ev-arg arg; ev-status 0; memset(ev-buf, 0, sizeof(ev-buf)); ev-len 0; ev-last_active time(NULL); //调用eventset函数的时间 return; }/* 向 epoll监听的红黑树 添加一个 文件描述符 *///eventadd(efd, EPOLLIN, g_events[MAX_EVENTS]);void eventadd(int efd, int events, struct myevent_s *ev){ struct epoll_event epv {0, {0}}; int op; epv.data.ptr ev; epv.events ev-events events; //EPOLLIN 或 EPOLLOUT if (ev-status 0) { //已经在红黑树 g_efd 里 op EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1 ev-status 1; } if (epoll_ctl(efd, op, ev-fd, epv) 0) //实际添加/修改 printf(event add failed [fd%d], events[%d]\n, ev-fd, events); else printf(event add OK [fd%d], op%d, events[%0X]\n, ev-fd, op, events); return ; }/* 从epoll 监听的 红黑树中删除一个 文件描述符*/void eventdel(int efd, struct myevent_s *ev){ struct epoll_event epv {0, {0}}; if (ev-status ! 1) //不在红黑树上 return ; //epv.data.ptr ev; epv.data.ptr NULL; ev-status 0; //修改状态 epoll_ctl(efd, EPOLL_CTL_DEL, ev-fd, epv); //从红黑树 efd 上将 ev-fd 摘除 return ; }/* 当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */void acceptconn(int lfd, int events, void *arg){ struct sockaddr_in cin; socklen_t len sizeof(cin); int cfd, i; if ((cfd accept(lfd, (struct sockaddr *)cin, len)) -1) { if (errno ! EAGAIN errno ! EINTR) { /* 暂时不做出错处理 */ } printf(%s: accept, %s\n, __func__, strerror(errno)); return ; } do { for (i 0; i MAX_EVENTS; i) //从全局数组g_events中找一个空闲元素 if (g_events[i].status 0) //类似于select中找值为-1的元素 break; //跳出 for if (i MAX_EVENTS) { printf(%s: max connect limit[%d]\n, __func__, MAX_EVENTS); break; //跳出do while(0) 不执行后续代码 } int flag 0; if ((flag fcntl(cfd, F_SETFL, O_NONBLOCK)) 0) { //将cfd也设置为非阻塞 printf(%s: fcntl nonblocking failed, %s\n, __func__, strerror(errno)); break; } /* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */ eventset(g_events[i], cfd, recvdata, g_events[i]); eventadd(g_efd, EPOLLIN, g_events[i]); //将cfd添加到红黑树g_efd中,监听读事件 } while(0); printf(new connect [%s:%d][time:%ld], pos[%d]\n, inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i); return ; }void recvdata(int fd, int events, void *arg){ struct myevent_s *ev (struct myevent_s *)arg; int len; len recv(fd, ev-buf, sizeof(ev-buf), 0); //读文件描述符, 数据存入myevent_s成员buf中 eventdel(g_efd, ev); //将该节点从红黑树上摘除 if (len 0) { ev-len len; ev-buf[len] \0; //手动添加字符串结束标记 printf(C[%d]:%s\n, fd, ev-buf); eventset(ev, fd, senddata, ev); //设置该 fd 对应的回调函数为 senddata eventadd(g_efd, EPOLLOUT, ev); //将fd加入红黑树g_efd中,监听其写事件 } else if (len 0) { close(ev-fd); /* ev-g_events 地址相减得到偏移元素位置 */ printf([fd%d] pos[%ld], closed\n, fd, ev-g_events); } else { close(ev-fd); printf(recv[fd%d] error[%d]:%s\n, fd, errno, strerror(errno)); } return; }void senddata(int fd, int events, void *arg){ struct myevent_s *ev (struct myevent_s *)arg; int len; len send(fd, ev-buf, ev-len, 0); //直接将数据 回写给客户端。未作处理 eventdel(g_efd, ev); //从红黑树g_efd中移除 if (len 0) { printf(send[fd%d], [%d]%s\n, fd, len, ev-buf); eventset(ev, fd, recvdata, ev); //将该fd的 回调函数改为 recvdata eventadd(g_efd, EPOLLIN, ev); //从新添加到红黑树上 设为监听读事件 } else { close(ev-fd); //关闭链接 printf(send[fd%d] error %s\n, fd, strerror(errno)); } return ; }/*创建 socket, 初始化lfd */void initlistensocket(int efd, short port){ struct sockaddr_in sin; int lfd socket(AF_INET, SOCK_STREAM, 0); fcntl(lfd, F_SETFL, O_NONBLOCK); //将socket设为非阻塞 memset(sin, 0, sizeof(sin)); //bzero(sin, sizeof(sin)) sin.sin_family AF_INET; sin.sin_addr.s_addr INADDR_ANY; sin.sin_port htons(port); bind(lfd, (struct sockaddr *)sin, sizeof(sin)); listen(lfd, 20); /* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */ eventset(g_events[MAX_EVENTS], lfd, acceptconn, g_events[MAX_EVENTS]); /* void eventadd(int efd, int events, struct myevent_s *ev) */ eventadd(efd, EPOLLIN, g_events[MAX_EVENTS]); return ; }int main(int argc, char *argv[]){ unsigned short port SERV_PORT; if (argc 2) port atoi(argv[1]); //使用用户指定端口.如未指定,用默认端口 g_efd epoll_create(MAX_EVENTS1); //创建红黑树,返回给全局 g_efd if (g_efd 0) printf(create efd in %s err %s\n, __func__, strerror(errno)); initlistensocket(g_efd, port); //初始化监听socket struct epoll_event events[MAX_EVENTS1]; //保存已经满足就绪事件的文件描述符数组 printf(server running:port[%d]\n, port); int checkpos 0, i; while (1) { /* 超时验证每次测试100个链接不测试listenfd 当客户端60秒内没有和服务器通信则关闭此客户端链接 */ long now time(NULL); //当前时间 for (i 0; i 100; i, checkpos) { //一次循环检测100个。 使用checkpos控制检测对象 if (checkpos MAX_EVENTS) checkpos 0; if (g_events[checkpos].status ! 1) //不在红黑树 g_efd 上 continue; long duration now - g_events[checkpos].last_active; //客户端不活跃的世间 if (duration 60) { close(g_events[checkpos].fd); //关闭与该客户端链接 printf([fd%d] timeout\n, g_events[checkpos].fd); eventdel(g_efd, g_events[checkpos]); //将该客户端 从红黑树 g_efd移除 } } /*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/ int nfd epoll_wait(g_efd, events, MAX_EVENTS1, 1000); if (nfd 0) { printf(epoll_wait error, exit\n); break; } for (i 0; i nfd; i) { /*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/ struct myevent_s *ev (struct myevent_s *)events[i].data.ptr; 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); 操作监听红黑树epfdepoll_create 函数的返回值。 epfdop对该监听红黑数所做的操作。EPOLL_CTL_ADD 添加fd到 监听红黑树EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下取消监听fd待监听的fdevent 本质 struct epoll_event 结构体 地址成员 eventsEPOLLIN / 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); 阻塞监听。epfdepoll_create 函数的返回值。 epfdevents传出参数【数组】 满足监听条件的 哪些 fd 结构体。maxevents数组 元素的总个数。 1024struct epoll_event evnets[1024]timeout-1: 阻塞0 不阻塞0: 超时时间 毫秒返回值 0: 满足监听的 总个数。 可以用作循环上限。0 没有fd满足监听事件-1失败。 errnoepoll实现多路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 ep1024 -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 -- while1---- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足 -- read() --- 小-大 -- write回去。反应堆不但要监听 cfd 的读事件、还要监听cfd的写事件。socket、bind、listen -- epoll_create 创建监听 红黑树 -- 返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while1---- 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 -------------------17096P-管理者线程 4. adjust_thread 循环 10 s 执行一次。进入管理者线程回调函数接收参数 void *arg --》 pool 结构体加锁 --》lock --》 整个结构体锁获取管理线程池要用的到 变量。 task_num, live_num, busy_num根据既定算法使用上述3变量判断是否应该 创建、销毁线程池中 指定步长的线程。97P-threadpool_add函数 5. threadpool_add () 总功能模拟产生任务。 num[20]设置回调函数 处理任务。 sleep1 代表处理完成。内部实现加锁初始化 任务队列结构体成员。 回调函数 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(); ---被舍弃serverlfd socket(AF_INET, STREAM, 0); SOCK_DGRAM --- 报式协议。bind();listen(); --- 可有可无while1{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_addrstruct 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_addrstruct 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 8000int main(void){ struct sockaddr_in serv_addr, clie_addr; socklen_t clie_addr_len; int sockfd; char buf[BUFSIZ]; char str[INET_ADDRSTRLEN]; int i, n; sockfd socket(AF_INET, SOCK_DGRAM, 0); bzero(serv_addr, sizeof(serv_addr)); serv_addr.sin_family AF_INET; serv_addr.sin_addr.s_addr htonl(INADDR_ANY); serv_addr.sin_port htons(SERV_PORT); bind(sockfd, (struct sockaddr *)serv_addr, sizeof(serv_addr)); printf(Accepting connections ...\n); while (1) { clie_addr_len sizeof(clie_addr); n recvfrom(sockfd, buf, BUFSIZ,0, (struct sockaddr *)clie_addr, clie_addr_len); if (n -1) perror(recvfrom error); printf(received from %s at PORT %d\n, inet_ntop(AF_INET, clie_addr.sin_addr, str, sizeof(str)), ntohs(clie_addr.sin_port)); for (i 0; i n; i) buf[i] toupper(buf[i]); n sendto(sockfd, buf, n, 0, (struct sockaddr *)clie_addr, sizeof(clie_addr)); if (n -1) perror(sendto error); } close(sockfd); return 0; }
下面是客户端代码
#include stdio.h#include string.h#include unistd.h#include arpa/inet.h#include ctype.h#define SERV_PORT 8000int 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); 参数 domainAF_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] \0; /* 确保打印时,没有乱码出现 */ printf(client bind filename %s\n, cliaddr.sun_path); while ((size read(cfd, buf, sizeof(buf))) 0) { for (i 0; i size; i) buf[i] toupper(buf[i]); write(cfd, buf, size); } close(cfd); } close(lfd); return 0; }
客户端代码
#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网络编程资料\day5\1-教学资料\课堂笔记.txt
108P-总结 直接看课程笔记linux网络编程资料\day5\1-教学资料\课堂笔记.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 *evstruct event *event_new(struct event_base *baseevutil_socket_t fdshort whatevent_callback_fn cb; void *arg);base event_base_new()返回值。fd 绑定到 event 上的 文件描述符what对应的事件r、w、eEV_READ 一次 读事件EV_WRTIE 一次 写事件EV_PERSIST 持续触发。 结合 event_base_dispatch 函数使用生效。cb一旦事件满足监听条件回调的函数。typedef void (*event_callback_fn)(evutil_socket_t fd, short, void *) arg 回调的函数的参数。返回值成功创建的 event116P-事件event操作 添加事件到 event_base
int event_add(struct event *ev, const struct timeval *tv);ev: event_new() 的返回值。tvNULL销毁事件
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 %s\n, 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); // while1 { 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-%d\n, 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 *evstruct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options);base event_basefd: 封装到bufferevent内的 fdoptionsBEV_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_newfd 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 事件对象封装了fdaddress、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: %s\n, buf); char *p 我是服务器, 已经成功收到你发送的数据!; // 发数据给客户端 bufferevent_write(bev, p, strlen(p)1); sleep(1); }// 写缓冲区回调void write_cb(struct bufferevent *bev, void *arg){ printf(Im服务器, 成功写数据给客户端,写缓冲区回调函数被回调...\n); }// 事件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); } 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 client\n”);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.hvoid 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: %s\n, buf); // 借助写缓冲写数据回给客户端 bufferevent_write(bev, abcdefg, 7); }// 写事件回调void write_cb(struct bufferevent *bev, void *arg){ printf(-------fwq------has wrote\n); }// 其他事件回调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.hvoid read_cb(struct bufferevent *bev, void *arg){ char buf[1024] {0}; bufferevent_read(bev, buf, sizeof(buf)); printf(fwq say:%s\n, 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 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); }// 客户端与用户交互从终端读取数据写给服务器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, len1); }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网络编程资料\day6\1-教学资料\课堂笔记.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 2048int init_listen_fd(int port, int epfd){ // 创建监听的套接字 lfd int lfd socket(AF_INET, SOCK_STREAM, 0); if (lfd -1) { perror(socket error); exit(1); } // 创建服务器地址结构 IPport 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); } // 打印客户端IPport char client_ip[64] {0}; printf(New Client IP: %s, Port: %d, cfd %d\n, 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 (i0; iret; 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 path\n); } // 获取用户输入的端口 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.1\r\n 2. Host: localhost:2222\r\n 3. User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:24.0) Gecko/201001 01 Firefox/24.0\r\n 4. Accept: text/html,application/xhtmlxml,application/xml;q0.9,/;q0.8\r\n 5. Accept-Language: zh-cn,zh;q0.8,en-us;q0.5,en;q0.3\r\n 6. Accept-Encoding: gzip, deflate\r\n 7. Connection: keep-alive\r\n 8. If-Modified-Since: Fri, 18 Jul 2014 08:36:36 GMT\r\n 【空行】\r\n
应答协议
Http1.1 200 OK 2. Server: xhttpd Content-Typetext/plain; charsetiso-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 \r\n [数据起始。。。。。 。。。。 。。。数据终止]
140P-单文件通信流程分析 getline() 获取 http协议的第一行。 从首行中拆分 GET、文件名、协议版本。 获取用户请求的文件名。 判断文件是否存在。 stat() 判断是文件还是目录。 是文件-- open – read – 写回给浏览器 先写 http 应答协议头 http/1.1 200 ok Content-Typetext/plain; charsetiso-8859-1141P-处理出错返回
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%s\n, 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 path1; // 取出 客户端要访问的文件名 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; charsetiso-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 %s\r\n, no, disp); send(cfd, buf, strlen(buf), 0); sprintf(buf, Content-Type: %s\r\n, type); sprintf(bufstrlen(buf), Content-Length:%d\r\n, len); send(cfd, buf, strlen(buf), 0); send(cfd, \r\n, 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: %d\n, ret); } close(fd); }
146P-文件类型区分
147P-错误原因及说明 MP3请求错误的原因在于做错误判断时太粗略errnoEAGAIN或者errnoEINTR时并不算错误此时继续执行循环读取数据就行。 然而原来的程序是直接退出了所以没接收到数据。
148P-错误页面展示
错误页面部分的代码
void send_error(int cfd, int status, char *title, char *text){ char buf[4096] {0}; sprintf(buf, %s %d %s\r\n, HTTP/1.1, status, title); sprintf(bufstrlen(buf), Content-Type:%s\r\n, text/html); sprintf(bufstrlen(buf), Content-Length:%d\r\n, -1); sprintf(bufstrlen(buf), Connection: close\r\n); send(cfd, buf, strlen(buf), 0); send(cfd, \r\n, 2, 0); memset(buf, 0, sizeof(buf)); sprintf(buf, htmlheadtitle%d %s/title/head\n, status, title); sprintf(bufstrlen(buf), body bgcolor\#cc99cc\h2 align\center\%d %s/h4\n, status, title); sprintf(bufstrlen(buf), %s\n, text); sprintf(bufstrlen(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 %s\n, method, path, protocol); // 转码 将不能识别的中文乱码 - 中文 // 解码 %23 %34 %5f decode_str(path, path); char* file path1; // 去掉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; charsetutf-8; if (strcmp(dot, .html) 0 || strcmp(dot, .htm) 0) return text/html; charsetutf-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; charsetutf-8; }
152P-汉字字符编码和解码 URL中的汉字默认是存为Unicode码
153P-libevent实现的web服务器 直接源码啃起来吧
154P-telnet调试