1. 1. 网络编程
    1. 1.1. day1笔记
    2. 1.2. socket
      1. 1.2.1. TCP三次握手
      2. 1.2.2. 四次握手 断开连接
      3. 1.2.3. 三次握手与函数的对应关系:
      4. 1.2.4. 三次握手四次挥手与TCP状态对应关系:
        1. 1.2.4.1. 端口复用:
        2. 1.2.4.2. 心跳包:
        3. 1.2.4.3. 主动关闭端的2MSL时长:
      5. 1.2.5. 半关闭
    3. 1.3. day2笔记
    4. 1.4. 多路IO转接(响应式)服务器
      1. 1.4.1. select函数实现响应式服务器
        1. 1.4.1.1. select函数
        2. 1.4.1.2. 设置集合的函数:
        3. 1.4.1.3. 思路分析:
        4. 1.4.1.4. select实现代码:
        5. 1.4.1.5. select优缺点:
    5. 1.5. day3笔记
      1. 1.5.1. poll函数实现响应式服务器(了解)
        1. 1.5.1.1. poll函数
        2. 1.5.1.2. 思路分析:
        3. 1.5.1.3. poll实现代码
        4. 1.5.1.4. poll优缺点:
          1. 1.5.1.4.1. 突破 1024 文件描述符限制:
      2. 1.5.2. epoll函数实现响应式服务器(重要)
        1. 1.5.2.1. epoll函数
        2. 1.5.2.2. 思路分析
        3. 1.5.2.3. epoll实现代码
        4. 1.5.2.4. epoll的触发模式:
        5. 1.5.2.5. readn阻塞:
          1. 1.5.2.5.1. 如果要在ET的情况下用fcntl函数实现非阻塞:
            1. 1.5.2.5.1.1. 设置阻塞非阻塞:
        6. 1.5.2.6. epoll优缺点:
        7. 1.5.2.7. epoll反应堆模型
    6. 1.6. day4笔记
    7. 1.7. 线程池
      1. 1.7.1. 线程池的步骤
      2. 1.7.2. 线程池相关函数
      3. 1.7.3. 线程池模块分析:
      4. 1.7.4. 实现代码:
    8. 1.8. TCP通信和UDP通信各自的优缺点:
    9. 1.9. UDP通信
      1. 1.9.1. UDP实现的 C/S 模型:
        1. 1.9.1.1. 服务器代码
        2. 1.9.1.2. 客户端代码
    10. 1.10. 本地套接字和网络套接字
    11. 1.11. IPC通信:
    12. 1.12. 网络通信:
    13. 1.13. Libevent库
      1. 1.13.1. 本地套接字对比网络编程 TCP C/S模型
      2. 1.13.2. 实现代码
        1. 1.13.2.1. 服务器代码:
        2. 1.13.2.2. 客户端代码
    14. 1.14. libevent库
      1. 1.14.1. 参考 README、readme
      2. 1.14.2. 特性:
        1. 1.14.2.1. 同步/异步通信模型
        2. 1.14.2.2. 函数指针
      3. 1.14.3. 未决和非未决:
      4. 1.14.4. 带缓冲区的事件 bufferevent
      5. 1.14.5. bufferevent相关函数
      6. 1.14.6. 启动和关闭缓冲区
    15. 1.15. web大练习
      1. 1.15.1. 准备工作:html语言
      2. 1.15.2. http超文本传输协议

网络编程

day1笔记

协议:
一组规则。

分层模型结构:

1
2
3
4
5
6
OSI七层模型:  物、数、网、传、会、表、应
TCP/IP 4层模型:网(链路层/网络接口层)、网、传、应
应用层:http、ftp、nfs、ssh、telnet。。。
传输层:TCP、UDP
网络层:IP、ICMP、IGMP
链路层:以太网帧协议、ARP

c/s模型:

1
client-server

b/s模型:

1
2
3
4
5
6
7
8
browser-server
C/S B/S

优点: 缓存大量数据、协议选择灵活 安全性、跨平台、开发工作量较小

速度快

缺点: 安全性、跨平台、开发工作量较大 不能缓存大量数据、严格遵守 http

网络传输流程:

1
2
3
数据没有封装之前,是不能在网络中传递。
数据-》应用层-》传输层-》网络层-》链路层 --- 网络环境
以太网帧协议:
1
2
3
ARP协议:根据 Ip 地址获取 mac 地址。

以太网帧协议:根据mac地址,完成数据包传输。

IP协议:

1
2
3
4
5
6
7
版本: 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

1
16位:目的端口号。

TCP协议:

1
2
3
4
5
6
16位:源端口号。	2^16 = 65536  
16位:目的端口号。
32序号;
32确认序号。
6个标志位。
16位窗口大小。 2^16 = 65536

网络套接字: socket

1
2
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现。)
在通信过程中, 套接字一定是成对出现的。

网络字节序:

1
2
3
4
5
6
小端法:(pc本地存储)	高位存高地址。地位存低地址。	int a = 0x12345678
大端法:(网络存储) 高位存低地址。地位存高地址。
htonl --> 本地--》网络 (IP) 192.168.1.11 --> string --> atoi --> int --> htonl --> 网络字节序
htons --> 本地--》网络 (port)
ntohl --> 网络--》 本地(IP)
ntohs --> 网络--》 本地(Port)

IP地址转换函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int inet_pton(int af, const char *src, void *dst);		本地字节序(string IP) ---> 网络字节序
af:AF_INET、AF_INET6
src:传入,IP地址(点分十进制)
dst:传出,转换后的 网络字节序的 IP地址。
返回值:
成功: 1
异常: 0, 说明src指向的不是一个有效的ip地址。
失败:-1
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); 网络字节序 ---> 本地字节序(string IP)
af:AF_INET、AF_INET6
src: 网络字节序IP地址
dst:本地字节序(string IP)
size: dst 的大小。
返回值: 成功:dst。
失败:NULL

sockaddr地址结构: IP + port –> 在网络环境中唯一标识一个进程。

1
2
3
4
5
6
7
8
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6 man 7 ip
addr.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函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/socket.h>
int socket(int domain, int type, int protocol); 创建一个 套接字
domain:AF_INET、AF_INET6、AF_UNIX
type:SOCK_STREAM、SOCK_DGRAM
protocol: 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 *)&addr
addrlen: sizeof(addr) 地址结构的大小。
返回值:
成功:0
失败:-1 errno
int listen(int sockfd, int backlog); 设置同时与服务器建立连接的上限数。(同时进行3次握手的客户端数量)
sockfd: socket 函数返回值
backlog:上限数值。最大值 128.
1
2
3
4
5
6
7
8
9
10
11
12
	返回值:
成功: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
1
2
3
4
5
6
7
  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:传入参数。服务器的地址结构

1
2
3
4
5
addrlen:服务器的地址结构的大小
返回值:
成功:0
失败:-1 errno
如果不使用bind绑定客户端地址结构, 采用"隐式绑定".

TCP通信流程分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
1. socket() 创建socket
2. bind() 绑定服务器地址结构
3. listen() 设置监听上限
4. accept() 阻塞监听客户端连接
5. read(fd) 读socket获取客户端数据
6. 小--大写 toupper()
7. write(fd)
8. close();

client:
1. socket() 创建socket
2. connect(); 与服务器建立连接
3. write() 写数据到 socket
4. read() 读转换后的数据。
5. 显示读取结果
6. close()

socket

1708420606702

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.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;
char buf[BUFSIZ];
struct sockaddr_in serv_addr, clit_addr;
socklen_t clit_addr_len;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//建立socket
lfd = socket(AF_INET,SOCK_STREAM,0);
if (lfd == -1) {
sys_err("socket error");
}
//bind 绑定ip+port
bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//listen 设置监听上限
listen(lfd,128);
clit_addr_len = sizeof(clit_addr);
//accept 阻塞直到有客户端数据连接
cfd = accept(lfd, (struct sockaddr*)&clit_addr, &clit_addr_len);
if (cfd == -1)
{
sys_err("accept error");
}


while (1)
{
ret = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, ret);
for (int i = 0; i < ret; i++)
{
buf[i] = toupper(buf[i]);
}
write(cfd, buf, ret);
}

close(lfd);
close(cfd);

}

第一个窗口

1708400293830

第二个窗口

nc 127.0.0.1 9527 客户端向服务器发送信息

1708400261305

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.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;
char buf[BUFSIZ],client_IP[1024];
struct sockaddr_in serv_addr, clit_addr;
socklen_t clit_addr_len;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//建立socket
lfd = socket(AF_INET,SOCK_STREAM,0);
if (lfd == -1) {
sys_err("socket error");
}
//bind 绑定ip+port
bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//listen 设置监听上限
listen(lfd,128);
clit_addr_len = sizeof(clit_addr);
//accept 阻塞直到有客户端数据连接
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));

while (1)
{
ret = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, ret);
for (int i = 0; i < ret; i++)
{
buf[i] = toupper(buf[i]);
}
write(cfd, buf, ret);
}

close(lfd);
close(cfd);

}

客户端程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#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, "hello\n", 6);
ret = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, ret);
sleep(1);
}

close(cfd);

return 0;
}

TCP三次握手

三次握手 建立连接

![1708422833527](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708422833527.png)

![1708422817941](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708422817941.png)

四次握手 断开连接

![1708431321844](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708431321844.png)

导致四次握手才能断开连接的原因是什么:半关闭

每个socket套接字里面有两个缓冲区:写数据的缓冲区和都数据的缓冲区,

在客户端发送FIN信号之后,服务器端同意了,然后客户端关闭自己的写缓冲区,但是连接还是存在的,它可以发送不在写缓冲区的信号,比如在TCP头部的ACK信号

![1708431678033](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708431678033.png)

客户端:我不说话啦?

服务器:嗯

客户端:闭上了自己的嘴(但是两人的电话还没挂断)

除了SYN、ACK、FIN信号,还有一个滑动窗口win:用来告诉对方我能接收的最大数据是多少,够了这个数据就不要发了

![1708432554044](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708432554044.png)

这里假定左边是客户端右边是服务器,客户端是4096服务器是4144,发到第9个就不再发了,因为客户端发到第9个已经发了5121+1024-1=6144了,再发就超了

![1708432889242](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708432889242.png)

然后后面服务器不断处理读缓冲区的数据,从6144-6144=0变为2048再变为4096

滑动窗口就是为了防止数据丢失

三次握手与函数的对应关系:

![1708435420246](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708435420246.png)

三次握手四次挥手与TCP状态对应关系:

(参考上面那张图好看)

![1708501001865](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708501001865.png)

三次握手四次挥手与TCP状态对应关系

![1708502991908](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708502991908.png)

只有主动关闭连接这一方会经历FIN_WAIT2(半关闭)和TIME_WAIT状态和2MSL时长才能关闭

只有被动关闭一方有CLOSE_WAIT,此时主动关闭一方处于版关闭状态

所以如果先关闭服务器,想要接着重启会报错Address is already in use,因为他是主动关闭连接一方正在等待2MSL时长,而如果先关闭客户端,因为他的端口是系统自动分配的不固定,所以没事

端口复用:

服务器是在bind函数之前(可能客户端在connect函数之前)

1
2
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void *)&opt,sizeof(opt));

SO_REUSEADDR 端口复用

心跳包:

如果对方异常断开,本机检测不到,一直等待,浪费资源,需要设置TCP的保持连接,作用就是每隔一定的时间间隔发送探测分节,如果连续发送多个探测分节对方还未回应,就将次连接断开

1
2
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_KEEPALIVE,(void *)&opt,sizeof(opt));

SO_KEEPALIVE表示保持连接,如果两个小时内这个套接字的口没有数据交换,发一次报文,对方如果不回复就自动关闭这个进程

也可以不用SO_KEEPALIVE自己写一个规定时间

心跳包:最小粒度

乒乓包:携带比较多的数据的心跳包

主动关闭端的2MSL时长:

保证最后一个ACK能成功被对端接收。(等待期间,对端没收到我发的ACK,对端会再次发送FIN请求。)

半关闭

通信双方中,只有一端关闭通信。——FIN_WAIT_2

关闭使用close(cfd)或者shutdown(cfd,SHUT_RD)

两者的区别:

shutdown可以更精细的关闭写缓冲区或者读缓冲区

如果使用dup2(cfd,cfd1);

当使用close(cfd)之后虽然cfd关闭了但是cfd1还可以用

使用shutdown(cfd,SHUT_RD)cfd1也不能用了

day2笔记

三次握手:

1
2
3
4
5
主动发起连接请求端,发送 SYN 标志位,请求建立连接。 携带序号号、数据字节数(0)、滑动窗口大小。

被动接受连接请求端,发送 ACK 标志位,同时携带 SYN 请求标志位。携带序号、确认序号、数据字节数(0)、滑动窗口大小。

主动发起连接请求端,发送 ACK 标志位,应答服务器连接请求。携带确认序号。

四次挥手:

1
2
3
主动关闭连接请求端, 发送 FIN 标志位。 

被动关闭连接请求端, 应答 ACK 标志位。 ----- 半关闭完成。
1
2
3
被动关闭连接请求端, 发送 FIN 标志位。

主动关闭连接请求端, 应答 ACK 标志位。 ----- 连接全部关闭


滑动窗口:

1
发送给连接对端,本端的缓冲区大小(实时),保证数据不会丢失。

错误处理函数:

1
2
3
封装目的: 

在 server.c 编程过程中突出逻辑,将出错处理与逻辑分开,可以直接跳转man手册。
1
【wrap.c】								【wrap.h】
1
2
3
4
5
6
7
8
9
10
11
12
13
存放网络通信相关常用 自定义函数						存放 网络通信相关常用 自定义函数原型(声明)。

命名方式:系统调用函数首字符大写, 方便查看man手册

如:Listen()、Accept();

函数功能:调用系统调用函数,处理出错场景。

在 server.c 和 client.c 中调用 自定义函数

联合编译 server.c 和 wrap.c 生成 server

client.c 和 wrap.c 生成 client

readn:
读 N 个字节

readline:

1
读一行

read 函数的返回值:

1
2
3
4
5
6
7
8
9
10
11
1. > 0 实际读到的字节数

2. = 0 已经读到结尾(对端已经关闭)【 !重 !点 !】

3. -1 应进一步判断errno的值:

errno = EAGAIN or EWOULDBLOCK: 设置了非阻塞方式 读。 没有数据到达。

errno = EINTR 慢速系统调用被 中断。

errno = “其他情况” 异常。

多进程并发服务器:server.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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) 关闭用于建立连接的套接字 lfd

read()
小--大
write()

} else if (pid > 0) {

close(cfd); 关闭用于与客户端通信的套接字 cfd
contiue;
}
}

5. 子进程:

close(lfd)

read()

小--大

write()

父进程:

close(cfd);

注册信号捕捉函数: SIGCHLD

在回调函数中, 完成子进程回收

while (waitpid());

多线程并发服务器: server.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1. Socket();		创建 监听套接字 lfd

2. 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) 不能关闭。 主线程要使用lfd

read(cfd)

小--大

write(cfd)

pthread_exit((void *)10);
}

多路IO转接(响应式)服务器

设计两个监听 lfd和cfd

lfd是系统创建的,创建好交给小秘书让他监听,是否有人跟我连接,如果有,再调用accept函数,这样accept不用一直处于阻塞状态

cfd监听是都进行数据通信

过程:

如果有c2想向我连接,他先找小秘书,小秘书再找我,然后我调accept函数,就可以直接返回一个cfd2,不用阻塞等着,,然后产生的cfd2继续交给小秘书(select()函数)监听

三种响应模式:

响应式:上述带个秘书用来监听的模式(有学生听不懂问老师,老师再给解答)

阻塞式:一个进程在那里等着(老师问学生有没有听不懂,学生不回答,老师就一直等着)

非阻塞忙轮询:(老师问学生有没有听不懂,学生不回答,老师回去忙5分钟再去问学生)

小秘书有poll()epoll(), select()

select函数实现响应式服务器

select函数

原理: 借助内核, select 来监听, 客户端连接、数据通信事件。

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

nfds:监听的所有文件描述符中,最大文件描述符+1

readfds: 读 文件描述符监听集合。 传入、传出参数

writefds:写 文件描述符监听集合。 传入、传出参数 NULL

exceptfds:异常 文件描述符监听集合 传入、传出参数 NULL

timeout: > 0: 设置监听超时时长。

​ NULL: 阻塞监听

​ 0: 非阻塞监听,轮询

​ 返回值:

​ > 0: 所有监听集合(3个)中, 满足对应事件的总数。

​ 0: 没有满足监听条件的文件描述符

​ -1: errno

​ 在执行完select函数之后,前面的文件描述符集合也会变化,变成发生事件的文件描述符,比如之前读事件描述符的输入3、4、5,发生读事件的是4、5,文件描述符就变成4、5

![1708518269532](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708518269532.png)如图所示请求nfds=6+1

如果有新的c4发出connect请求,就是让lfd发出读时间,这个select监听就是在监听这些文件描述符有没有读事件

原理: 借助内核, select 来监听, 客户端连接、数据通信事件。

设置集合的函数:

传入select的是文件描述符集合,因此要把需要监听的文件描述符加入集合

1
2
3
4
5
6
7
8
9
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);
1
2
3
4
5
6
7
8
9
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);

思路分析:

一般之考虑读事件,lfd连接请求是读事件,cfd读也是读事件

而cfd写事件一般不考虑,因为一般客户端请求连接接通之后,服务器向客户端写一般是都可以的,除了客户端发送FIN信号之后处于半关闭状态或者服务器写的太快了数据量超过滑动窗口的这两种情况

int maxfd = 0;

lfd = socket() ; 创建套接字

maxfd = lfd;

bind(); 绑定地址结构

listen(); 设置监听上限

fd_set rset, allset; 创建r监听集合,一个里面只有lfd,一个是lfd和后来select加上的cfd

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)				检测新加入的cfd是否有read、write事件

        read()

        小 -- 大

        write();
    }	
}

}

select实现代码:

F:\a语法基础\C++\Linux\linux网络编程资料\day3\4-源代码\select_concurrent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#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 %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;
}


select优缺点:

缺点:	监听上限受文件描述符限制。 最大 1024.

    检测满足条件的fd, 自己添加业务逻辑提高小。 提高了编码难度。

优点:	跨平台。win、linux、macOS、Unix、类Unix、mips

day3笔记

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_2

close(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:监听的所有文件描述符中,最大文件描述符+1

    readfds: 读 文件描述符监听集合。	传入、传出参数

    writefds:写 文件描述符监听集合。	传入、传出参数		NULL

    exceptfds:异常 文件描述符监听集合	传入、传出参数		NULL

    timeout: 	> 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

poll函数实现响应式服务器(了解)

poll函数

poll是对select的改进,但是它是个半成品,相对select提升不大。最终版本是epoll,所以poll了解一下就完事儿,重点掌握epoll。

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

fds:监听的文件描述符【数组】

​ struct pollfd {

​ int fd: 待监听的文件描述符

​ short_events: 待监听的文件描述符对应的监听事件

​ 取值:POLLIN、POLLOUT、POLLERR

​ short revnets: 传入时, 给0。如果满足对应事件的话, 返回 非0 –> POLLIN、POLLOUT、POLLERR

​ }

​ struct pollfd *fds 中的fds是个数组,调用也是fds[0].fd,fds[0].short_events调用

​ nfds: 监听数组的,实际有效监听个数。

​ timeout: > 0: 超时时长。单位:毫秒。

​ -1: 阻塞等待

​ 0: 不阻塞

​ 返回值:返回满足对应监听事件的文件描述符 总个数。

​ 优点:

​ 自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。

​ 拓展 监听上限。 超出 1024限制。

​ 缺点:

​ 不能跨平台。 Linux

​ 无法直接定位满足监听事件的文件描述符, 编码难度较大。

思路分析:

![1708588840601](D:\Program Files (x86)\MyBlog\source_posts\网络编程\图片1.png)

poll实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/* 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 %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;
}

poll优缺点:

优点:

​ 自带数组结构。 可以将 监听事件集合 和 返回事件集合 分离。

​ 拓展 监听上限。 超出 1024限制。

​ 修改:
​ 打开 sudo vi /etc/security/limits.conf, 写入:

    * soft nofile 65536			--> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效】
    * hard nofile 100000			--> 命令修改上限。

缺点:

​ 不能跨平台。 Linux

​ 无法直接定位满足监听事件的文件描述符, 编码难度较大。(和select相同)

突破 1024 文件描述符限制:

​ 先查看:
​ cat /proc/sys/fs/file-max –> 当前计算机所能打开的最大文件个数。 受硬件影响。
​ ulimit -a ——> 当前用户下的进程,默认打开文件描述符个数。 缺省为 1024
​ ![1708604512946](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708604512946.png)

修改:
打开 sudo vi /etc/security/limits.conf, 写入:

    * soft nofile 65536			--> 设置默认值, 可以直接借助命令修改。 【注销用户,使其生效,往下调(1000->500)可以直接生效,往上调(1000->2000)需要注销用户,可以直接借用命令修改(open files后面有个(-n):ulimit -n 21000】

    * hard nofile 100000			--> 命令修改上限。来限制上面修改的最大值

![1708605538370](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708605538370.png)

epoll函数实现响应式服务器(重要)

使用有大量连接但是只有少量活跃的情况

epoll函数

(平衡二叉树里面的特例:红黑树)

1、创建一棵监听红黑树
int epoll_create(int size);						
    size:创建的红黑树的监听节点数量。(仅供内核参考。)
    返回值:指向新创建的红黑树的根节点的 fd。 
        失败: -1 errno
        
2、操作监听红黑树
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);	
    epfd:epoll_create 函数的返回值。 epfd
    op:对该监听红黑数所做的操作。
        EPOLL_CTL_ADD 添加fd到 监听红黑树
        EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。
        EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
    fd(fd111):
        待监听的fd		
    event:	本质 struct epoll_event 结构体 的地址,这个参数是一个传出参数,是要把这个结构体放到树上去,将来如果对应的事件满足的话,其中的fd就会被设置的后面的epoll_wait()函数里面
        成员 events:
            EPOLLIN / EPOLLOUT / EPOLLERR
        成员 data: 联合体(共用体):
            int fd;	  对应监听事件的 fd    对应前面的待监听的fd(fd111)
            void *ptr; 后面再讲  libevent通过把它打造成回调函数的模式(epoll反应堆模型),它和fd是共用体,和fd共享同一块地址空间
            uint32_t u32;  这个不用
            uint64_t u64;  这个不用 		
    返回值:成功 0; 失败: -1 errno
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
   	struct pollfd *fds 中的fds是个数组,调用也是fds[0].fd,fds[0].short_events调用,这个0也是一个结构体 的地址,这个fds作为数组就是这些地址的集合
   	数组是一连串连续的地址,数组名是首地址,指针指向下一个地址


3、阻塞监听。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 	
    epfd:epoll_create 函数的返回值。 epfd
    events:传出参数,【数组】, 满足监听条件的 那些 fd 结构体。,第二个函数中的event是一个结构体,而这个events是一个数组,用来存内核得到事件的集合
    maxevents:数组 元素的总个数。 1024			
        struct epoll_event evnets[1024]
    timeout:
        -1: 阻塞
        0: 不阻塞
        >0: 超时时间 (毫秒)
    返回值:
        > 0: 满足监听的 总个数。 可以用作循环上限。

        0: 没有fd满足监听事件

        -1:失败。 errno

思路分析

基本就是为kfd(cfd)创建一个结构体,然后挂到红黑树上,再在while(1)里面执行监听操作,如果有事件发生,再执行accept挂到红黑树(lfd)或者进行读写操作(cfd)

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实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>  
#include <stdlib.h>
#include <sys/epoll.h>
#include <errno.h>
#include <unistd.h>

#define MAXLINE 10

int main(int argc, char *argv[])
{
int efd, i;
int pfd[2];
pid_t pid;
char buf[MAXLINE], ch = 'a';

pipe(pfd);
pid = fork();

if (pid == 0) { //子 写
close(pfd[0]);
while (1) {
//aaaa\n
for (i = 0; i < MAXLINE/2; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//bbbb\n
for (; i < MAXLINE; i++)
buf[i] = ch;
buf[i-1] = '\n';
ch++;
//aaaa\nbbbb\n
write(pfd[1], buf, sizeof(buf));
sleep(5);
}
close(pfd[1]);

} else if (pid > 0) { //父 读
struct epoll_event event;
struct epoll_event resevent[10]; //epoll_wait就绪返回event
int res, len;

close(pfd[1]);
efd = epoll_create(10);

event.events = EPOLLIN | EPOLLET; // ET 边沿触发
// event.events = EPOLLIN; // LT 水平触发 (默认)
event.data.fd = pfd[0];
epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);

while (1) {
res = epoll_wait(efd, resevent, 10, -1);
printf("res %d\n", res);
if (resevent[0].data.fd == pfd[0]) {
len = read(pfd[0], buf, MAXLINE/2);
write(STDOUT_FILENO, buf, len);
}
}

close(pfd[0]);
close(efd);

} else {
perror("fork");
exit(-1);
}

return 0;
}

epoll的触发模式:

ET(edge-triggered)模式:

    边沿触发:

        缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。

        struct epoll_event event;

        event.events = EPOLLIN | EPOLLET;
LT(level triggered)模式:

    水平触发 -- 默认采用模式。

        缓冲区剩余未读尽的数据会导致 epoll_wait 返回。

两者的区别在于:

如果创建文件描述符,先写入”aaaa\n”,又写入”bbbb\n”,让这个文件描述符上树,在另一变只读取了”aaaa\n”,还剩下”bbbb\n”,如果是LT可以读,但是ET读不了,只有再往后写ET也读不了,只有再往后写”cccc\n”才能读”bbbb\n”

如果对方发来的1000个字节,我只需要读前500个字节剩下的想丢弃,那么我用ET模式,但是要ET就必须用非阻塞模式

server边沿触发,编译运行,结果如下:

​ ![1708588840601](D:\Program Files (x86)\MyBlog\source_posts\网络编程\图片2.png)

​ 运行后,每过5秒钟服务器才输出一组字符,这是就是边沿触发的效果。

更改服务器为水平触发模式,运行程序,如下:

​ ![1708588840601](D:\Program Files (x86)\MyBlog\source_posts\网络编程\图片3.png)

运行后,每5秒输出两组字符串,这是因为只写入了两组,这个模式的服务器,缓冲区有多少读多少。

结论:
    epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 --- 忙轮询。

    struct epoll_event event;

    event.events = EPOLLIN | EPOLLET;   //设置方式:原来是event.events = EPOLLIN 改成这个

    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);	

    int flg = fcntl(cfd, F_GETFL);	

    flg |= O_NONBLOCK;

    fcntl(cfd, F_SETFL, flg);

优点:

    高效。突破1024文件描述符。

缺点:
    不能跨平台。 Linux。

文件描述符:

文件描述符可以来源于管道运算符pipe(进程间通信),也可以来源于套接字socket(网络通信)

readn阻塞:

​ readn调用的阻塞,比如设定读500个字符,但是因为只发送了498,就只读到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).

如果要在ET的情况下用fcntl函数实现非阻塞:

![1708659878332](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708659878332.png)

三个参数分别是文件描述符、要干的事情和变参,两个作用,一是设置阻塞非阻塞,二是dup2(cfd,cfd1)函数复制文件描述符那里用过

设置阻塞非阻塞:

F_GETFL()把flag的值作为参数返回出来,这个flag的值本质上是一个位图,他有一个位表示阻塞还是非阻塞,默认是零的话给他位或变为1.

设置非阻塞模式之后,没有读到字节就直接返回了,但是可能读到498字节,剩下的两个字节是还没有发过来,如果想要再读剩下的两个字节,那么需要忙轮询

在设置边沿触发之后,用accept得到cfd,设置cfd非阻塞,添加下面三句代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
event.events = EPOLLIN | EPOLLET;   //设置方式:原来是event.events = EPOLLIN 改成这个

//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);
}

这样可以设置那个套接字为非阻塞状态,只要readn读完就返回而不是阻塞在那里,没有发送数据的话后面的epoll_wait该等还是等着

    epoll 的 ET模式, 高效模式,但是只支持 非阻塞模式。 --- 忙轮询。

    struct epoll_event event;
    event.events = EPOLLIN | EPOLLET;
    
    int flg = fcntl(cfd, F_GETFL);	
    flg |= O_NONBLOCK;
    fcntl(cfd, F_SETFL, flg);
    
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);	

epoll优缺点:

优点:
    高效。突破1024文件描述符。

缺点:
    不能跨平台。 Linux。

epoll反应堆模型

epoll ET模式 + 非阻塞、轮询 + void *ptr。

不看了79-84集https://www.bilibili.com/video/BV1iJ411S7UA?p=85&vd_source=4e66034f3b307a25122b6a56d66b54c8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);	
epfd:epoll_create 函数的返回值。 epfd
op:对该监听红黑数所做的操作。
EPOLL_CTL_ADD 添加fd到 监听红黑树
EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。
EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
fd(fd111):
待监听的fd
event: 本质 struct epoll_event 结构体 的地址,这个参数是一个传出参数,是要把这个结构体放到树上去,将来如果对应的事件满足的话,其中的fd就会被设置的后面的epoll_wait()函数里面
成员 events:
EPOLLIN / EPOLLOUT / EPOLLERR
成员 data: 联合体(共用体):
int fd; 对应监听事件的 fd 对应前面的待监听的fd(fd111)
void *ptr; 后面再讲 libevent通过把它打造成回调函数的模式(epoll反应堆模型),它和fd是共用体,和fd共享同一块地址空间
uint32_t u32; 这个不用
uint64_t u64; 这个不用
返回值:成功 0; 失败: -1 errno
如果我们定义一个结构体来封装fd
struct evt {
int fd
void (*func)(int fd)
}*ptr
就可以自动调用(回调)

原来

socket、bind、listen -- epoll_create 创建监听 红黑树 --  返回 epfd -- epoll_ctl() 向树上添加一个监听fd -- while(1)--

    -- epoll_wait 监听 -- 对应监听fd有事件产生 -- 返回 监听满足数组。 -- 判断返回数组元素 -- lfd满足 -- Accept -- cfd 满足 

    -- read() --- 小->大 -- write回去。


反应堆:不但要监听 cfd 的读事件、还要监听cfd的写事件。

反应堆的理解:加入IO转接之后,有了事件,server才去处理,这里反应堆也是这样,由于网络环境复杂,服务器处理数据之后,可能并不能直接写回去,比如遇到网络繁忙或者对方缓冲区已经满了这种情况,就不能直接写回给客户端。反应堆就是在处理数据之后,监听写事件,能写会客户端了,才去做写回操作。写回之后,再改为监听读事件。如此循环。

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 监听

day4笔记

多路IO转接:

select:

poll:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

    fds:监听的文件描述符【数组】

        struct pollfd {
            
            int fd:	待监听的文件描述符
            
            short events:	待监听的文件描述符对应的监听事件

                    取值:POLLIN、POLLOUT、POLLERR

            short 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 errno

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);	操作监听红黑树

    epfd:epoll_create 函数的返回值。 epfd

    op:对该监听红黑数所做的操作。

        EPOLL_CTL_ADD 添加fd到 监听红黑树

        EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。

        EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)

    fd:
        待监听的fd

    event:	本质 struct epoll_event 结构体 地址

        成员 events:

            EPOLLIN / EPOLLOUT / EPOLLERR

        成员 data: 联合体(共用体):

            int fd;	  对应监听事件的 fd

            void *ptr; 

            uint32_t u32;

            uint64_t u64;		

    返回值:成功 0; 失败: -1 errno


int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 	 阻塞监听。

    epfd:epoll_create 函数的返回值。 epfd

    events:传出参数,【数组】, 满足监听条件的 哪些 fd 结构体。

    maxevents:数组 元素的总个数。 1024
            
        struct epoll_event evnets[1024]
    timeout:

        -1: 阻塞

        0: 不阻塞

        >0: 超时时间 (毫秒)

    返回值:

        > 0: 满足监听的 总个数。 可以用作循环上限。

        0: 没有fd满足监听事件

        -1:失败。 errno

epoll实现多路IO转接思路:

lfd = socket(); 监听连接事件lfd
bind();
listen();

int epfd = epoll_create(1024); epfd, 监听红黑树的树根。

struct epoll_event tep, ep[1024]; tep, 用来设置单个fd属性, ep 是 epoll_wait() 传出的满足监听事件的数组。

tep.events = EPOLLIN; 初始化 lfd的监听属性。
tep.data.fd = lfd

epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep); 将 lfd 添加到监听红黑树上。

while (1) {

ret = epoll_wait(epfd, ep,1024, -1);			实施监听

for (i = 0; i < ret; i++) {
    
    if (ep[i].data.fd == lfd) {				// lfd 满足读事件,有新的客户端发起连接请求

        cfd = Accept();

        tep.events = EPOLLIN;				初始化  cfd的监听属性。
        tep.data.fd = cfd;

        epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);

    } 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();

线程池

![1708830507101](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708830507101.png)

如果服务器起一个线程去消化客户端产生的线程,这样就可以把服务器解放出来

但是多线程多进程比多路IO转接效率低

同时在一个进程中维护n给线程进程,在通信的时候cpu要在多个线程之间切换,系统资源消耗比较大

换成多路IO转接只需要一个进程线程就可以了,系统资源占用比较小,内核会自动帮你维护

也就是说每次创建线程销毁线程是开销大的一个原因,如果每次创建一堆线程(线程池),就可以减少开销

![1708831987508](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708831987508.png)

线程池的步骤

​ 首先创建一些线程池,里面有一些初始化的最小线程数、指定最大线程数,所有的线程都通过条件变量(任务队列是否为空)阻塞在任务队列上面,等任务满足了就自动调用回调函数去处理,任务多了就给线程池扩容,任务少了就给线程池瘦身,管理扩容和瘦身是用管理线程做的,管理线程借助忙线程数、活着的线程数和任务队列中的实际任务数之间按照某个算法算一个比例出来,达到某一个比例的时候,我们就要扩容或者瘦身。销毁线程是在任务队列里面发一个假消息去给阻塞在任务队列为空但是线程不为空的线程,让它以为任务来了,然后把要删除的线程数置成非零值,这样线程就会被wait唤醒,然后进入if语句判断销毁线程数不是0就会进入if语句里面,然后pthread就自动退出了。

线程池相关函数

struct threadpool_t {

pthread_mutex_t lock;               /* 用于锁住本结构体 */    
pthread_mutex_t thread_counter;     /* 记录忙状态线程个数de琐 -- busy_thr_num */

pthread_cond_t queue_not_full;      /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
pthread_cond_t queue_not_empty;     /* 任务队列里不为空时,通知等待任务的线程 */

pthread_t *threads;                 /* 存放线程池中每个线程的tid。数组 */
pthread_t adjust_tid;               /* 存管理线程tid */
threadpool_task_t *task_queue;      /* 任务队列(数组首地址) */

int min_thr_num;                    /* 线程池最小线程数 */
int max_thr_num;                    /* 线程池最大线程数 */
int live_thr_num;                   /* 当前存活线程个数 */
int busy_thr_num;                   /* 忙状态线程个数 */
int wait_exit_thr_num;              /* 要销毁的线程个数 */

int queue_front;                    /* task_queue队头下标 */
int queue_rear;                     /* task_queue队尾下标 */
int queue_size;                     /* task_queue队中实际任务数 */
int queue_max_size;                 /* task_queue队列可容纳任务数上限 */

int shutdown;                       /* 标志位,线程池使用状态,true或false */

};

typedef struct {

void *(*function)(void *);          /* 函数指针,回调函数 */
void *arg;                          /* 上面函数的参数 */

} threadpool_task_t; /* 各子线程任务结构体 */

rear = 5 % 5

线程池模块分析:

1. main();		

    创建线程池。

    向线程池中添加任务。 借助回调处理任务。

    销毁线程池。


​ 2. pthreadpool_create();

创建线程池结构体 指针。

    初始化线程池结构体 {  N 个成员变量 }

    创建 N 个任务线程。

    创建 1 个管理者线程。

    失败时,销毁开辟的所有空间。(释放)

3. threadpool_thread()

    进入子线程回调函数。

    接收参数 void *arg  --》 pool 结构体

    加锁 --》lock --》 整个结构体锁

    判断条件变量 --》 wait  -------------------170

4. adjust_thread()

    循环 10 s 执行一次。

    进入管理者线程回调函数

    接收参数 void *arg  --》 pool 结构体

    加锁 --》lock --》 整个结构体锁

    获取管理线程池要用的到 变量。	task_num, live_num, busy_num

    根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。

5. threadpool_add ()

    总功能:

        模拟产生任务。   num[20]

        设置回调函数, 处理任务。  sleep(1) 代表处理完成。

    内部实现:

        加锁

        初始化 任务队列结构体成员。   回调函数 function, arg

        利用环形队列机制,实现添加任务。 借助队尾指针挪移 % 实现。

        唤醒阻塞在 条件变量上的线程。

        解锁

6.  从 3. 中的wait之后继续执行,处理任务。

    加锁
    
    获取 任务处理回调函数,及参数

    利用环形队列机制,实现处理任务。 借助队头指针挪移 % 实现。

    唤醒阻塞在 条件变量 上的 server。

    解锁

    加锁 

    改忙线程数++

    解锁

    执行处理任务的线程

    加锁 

    改忙线程数——

    解锁

7. 创建 销毁线程

    管理者线程根据 task_num, live_num, busy_num  

    根据既定算法,使用上述3变量,判断是否应该 创建、销毁线程池中 指定步长的线程。

    如果满足 创建条件

        pthread_create();   回调 任务线程函数。		live_num++

    如果满足 销毁条件

        wait_exit_thr_num = 10;  

        signal 给 阻塞在条件变量上的线程 发送 假条件满足信号    

        跳转至  --170 wait阻塞线程会被 假信号 唤醒。判断: wait_exit_thr_num  > 0 pthread_exit();  

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include "threadpool.h"

#define DEFAULT_TIME 10 /*10s检测一次*/
#define MIN_WAIT_TASK_NUM 10 /*如果queue_size > MIN_WAIT_TASK_NUM 添加新的线程到线程池*/
#define DEFAULT_THREAD_VARY 10 /*每次创建和销毁线程的个数*/
#define true 1
#define false 0

typedef struct {
void *(*function)(void *); /* 函数指针,回调函数 */
void *arg; /* 上面函数的参数 */
} threadpool_task_t; /* 各子线程任务结构体 */

/* 描述线程池相关信息 */

struct threadpool_t {
pthread_mutex_t lock; /* 用于锁住本结构体 */
pthread_mutex_t thread_counter; /* 记录忙状态线程个数de琐 -- busy_thr_num */

pthread_cond_t queue_not_full; /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
pthread_cond_t queue_not_empty; /* 任务队列里不为空时,通知等待任务的线程 */

pthread_t *threads; /* 存放线程池中每个线程的tid。数组 */
pthread_t adjust_tid; /* 存管理线程tid */
threadpool_task_t *task_queue; /* 任务队列(数组首地址) */

int min_thr_num; /* 线程池最小线程数 */
int max_thr_num; /* 线程池最大线程数 */
int live_thr_num; /* 当前存活线程个数 */
int busy_thr_num; /* 忙状态线程个数 */
int wait_exit_thr_num; /* 要销毁的线程个数 */

int queue_front; /* task_queue队头下标 */
int queue_rear; /* task_queue队尾下标 */
int queue_size; /* task_queue队中实际任务数 */
int queue_max_size; /* task_queue队列可容纳任务数上限 */

int shutdown; /* 标志位,线程池使用状态,true或false */
};

void *threadpool_thread(void *threadpool);

void *adjust_thread(void *threadpool);

int is_thread_alive(pthread_t tid);
int threadpool_free(threadpool_t *pool);

//threadpool_create(3,100,100);
threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size)
{
int i;
threadpool_t *pool = NULL; /* 线程池 结构体 */

do {
if((pool = (threadpool_t *)malloc(sizeof(threadpool_t))) == NULL) {
printf("malloc threadpool fail");
break; /*跳出do while*/
}

pool->min_thr_num = min_thr_num;
pool->max_thr_num = max_thr_num;
pool->busy_thr_num = 0;
pool->live_thr_num = min_thr_num; /* 活着的线程数 初值=最小线程数 */
pool->wait_exit_thr_num = 0;
pool->queue_size = 0; /* 有0个产品 */
pool->queue_max_size = queue_max_size; /* 最大任务队列数 */
pool->queue_front = 0;
pool->queue_rear = 0;
pool->shutdown = false; /* 不关闭线程池 */

/* 根据最大线程上限数, 给工作线程数组开辟空间, 并清零 */
pool->threads = (pthread_t *)malloc(sizeof(pthread_t)*max_thr_num);
if (pool->threads == NULL) {
printf("malloc threads fail");
break;
}
memset(pool->threads, 0, sizeof(pthread_t)*max_thr_num);

/* 给 任务队列 开辟空间 */
pool->task_queue = (threadpool_task_t *)malloc(sizeof(threadpool_task_t)*queue_max_size);
if (pool->task_queue == NULL) {
printf("malloc task_queue fail");
break;
}

/* 初始化互斥琐、条件变量 */
if (pthread_mutex_init(&(pool->lock), NULL) != 0
|| pthread_mutex_init(&(pool->thread_counter), NULL) != 0
|| pthread_cond_init(&(pool->queue_not_empty), NULL) != 0
|| pthread_cond_init(&(pool->queue_not_full), NULL) != 0)
{
printf("init the lock or cond fail");
break;
}

/* 启动 min_thr_num 个 work thread */
for (i = 0; i < min_thr_num; i++) {
pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool); /*pool指向当前线程池*/
printf("start thread 0x%x...\n", (unsigned int)pool->threads[i]);
}
pthread_create(&(pool->adjust_tid), NULL, adjust_thread, (void *)pool); /* 创建管理者线程 */

return pool;

} while (0);

threadpool_free(pool); /* 前面代码调用失败时,释放poll存储空间 */

return NULL;
}

/* 向线程池中 添加一个任务 */
//threadpool_add(thp, process, (void*)&num[i]); /* 向线程池中添加任务 process: 小写---->大写*/

int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg)
{
pthread_mutex_lock(&(pool->lock));

/* ==为真,队列已经满, 调wait阻塞 */
while ((pool->queue_size == pool->queue_max_size) && (!pool->shutdown)) {
pthread_cond_wait(&(pool->queue_not_full), &(pool->lock));
}

if (pool->shutdown) {
pthread_cond_broadcast(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));
return 0;
}

/* 清空 工作线程 调用的回调函数 的参数arg */
if (pool->task_queue[pool->queue_rear].arg != NULL) {
pool->task_queue[pool->queue_rear].arg = NULL;
}

/*添加任务到任务队列里*/
pool->task_queue[pool->queue_rear].function = function;
pool->task_queue[pool->queue_rear].arg = arg;
pool->queue_rear = (pool->queue_rear + 1) % pool->queue_max_size; /* 队尾指针移动, 模拟环形 */
pool->queue_size++;

/*添加完任务后,队列不为空,唤醒线程池中 等待处理任务的线程*/
pthread_cond_signal(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));

return 0;
}

/* 线程池中各个工作线程 */
void *threadpool_thread(void *threadpool)
{
threadpool_t *pool = (threadpool_t *)threadpool;
threadpool_task_t task;

while (true) {
/* Lock must be taken to wait on conditional variable */
/*刚创建出线程,等待任务队列里有任务,否则阻塞等待任务队列里有任务后再唤醒接收任务*/
pthread_mutex_lock(&(pool->lock));

/*queue_size == 0 说明没有任务,调 wait 阻塞在条件变量上, 若有任务,跳过该while*/
while ((pool->queue_size == 0) && (!pool->shutdown)) {
printf("thread 0x%x is waiting\n", (unsigned int)pthread_self());
pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));

/*清除指定数目的空闲线程,如果要结束的线程个数大于0,结束线程*/
if (pool->wait_exit_thr_num > 0) {
pool->wait_exit_thr_num--;

/*如果线程池里线程个数大于最小值时可以结束当前线程*/
if (pool->live_thr_num > pool->min_thr_num) {
printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
pool->live_thr_num--;
pthread_mutex_unlock(&(pool->lock));

pthread_exit(NULL);
}
}
}

/*如果指定了true,要关闭线程池里的每个线程,自行退出处理---销毁线程池*/
if (pool->shutdown) {
pthread_mutex_unlock(&(pool->lock));
printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
pthread_detach(pthread_self());
pthread_exit(NULL); /* 线程自行结束 */
}

/*从任务队列里获取任务, 是一个出队操作*/
task.function = pool->task_queue[pool->queue_front].function;
task.arg = pool->task_queue[pool->queue_front].arg;

pool->queue_front = (pool->queue_front + 1) % pool->queue_max_size; /* 出队,模拟环形队列 */
pool->queue_size--;

/*通知可以有新的任务添加进来*/
pthread_cond_broadcast(&(pool->queue_not_full));

/*任务取出后,立即将 线程池琐 释放*/
pthread_mutex_unlock(&(pool->lock));

/*执行任务*/
printf("thread 0x%x start working\n", (unsigned int)pthread_self());
pthread_mutex_lock(&(pool->thread_counter)); /*忙状态线程数变量琐*/
pool->busy_thr_num++; /*忙状态线程数+1*/
pthread_mutex_unlock(&(pool->thread_counter));

(*(task.function))(task.arg); /*执行回调函数任务*/
//task.function(task.arg); /*执行回调函数任务*/

/*任务结束处理*/
printf("thread 0x%x end working\n", (unsigned int)pthread_self());
pthread_mutex_lock(&(pool->thread_counter));
pool->busy_thr_num--; /*处理掉一个任务,忙状态数线程数-1*/
pthread_mutex_unlock(&(pool->thread_counter));
}

pthread_exit(NULL);
}

/* 管理线程 */
void *adjust_thread(void *threadpool)
{
int i;
threadpool_t *pool = (threadpool_t *)threadpool;
while (!pool->shutdown) {

sleep(DEFAULT_TIME); /*定时 对线程池管理*/

pthread_mutex_lock(&(pool->lock));
int queue_size = pool->queue_size; /* 关注 任务数 */
int live_thr_num = pool->live_thr_num; /* 存活 线程数 */
pthread_mutex_unlock(&(pool->lock));

pthread_mutex_lock(&(pool->thread_counter));
int busy_thr_num = pool->busy_thr_num; /* 忙着的线程数 */
pthread_mutex_unlock(&(pool->thread_counter));

/* 创建新线程 算法: 任务数大于最小线程池个数, 且存活的线程数少于最大线程个数时 如:30>=10 && 40<100*/
if (queue_size >= MIN_WAIT_TASK_NUM && live_thr_num < pool->max_thr_num) {
pthread_mutex_lock(&(pool->lock));
int add = 0;

/*一次增加 DEFAULT_THREAD 个线程*/
for (i = 0; i < pool->max_thr_num && add < DEFAULT_THREAD_VARY
&& pool->live_thr_num < pool->max_thr_num; i++) {
if (pool->threads[i] == 0 || !is_thread_alive(pool->threads[i])) {
pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool);
add++;
pool->live_thr_num++;
}
}

pthread_mutex_unlock(&(pool->lock));
}

/* 销毁多余的空闲线程 算法:忙线程X2 小于 存活的线程数 且 存活的线程数 大于 最小线程数时*/
if ((busy_thr_num * 2) < live_thr_num && live_thr_num > pool->min_thr_num) {

/* 一次销毁DEFAULT_THREAD个线程, 隨機10個即可 */
pthread_mutex_lock(&(pool->lock));
pool->wait_exit_thr_num = DEFAULT_THREAD_VARY; /* 要销毁的线程数 设置为10 */
pthread_mutex_unlock(&(pool->lock));

for (i = 0; i < DEFAULT_THREAD_VARY; i++) {
/* 通知处在空闲状态的线程, 他们会自行终止*/
pthread_cond_signal(&(pool->queue_not_empty));
}
}
}

return NULL;
}

int threadpool_destroy(threadpool_t *pool)
{
int i;
if (pool == NULL) {
return -1;
}
pool->shutdown = true;

/*先销毁管理线程*/
pthread_join(pool->adjust_tid, NULL);

for (i = 0; i < pool->live_thr_num; i++) {
/*通知所有的空闲线程*/
pthread_cond_broadcast(&(pool->queue_not_empty));
}
for (i = 0; i < pool->live_thr_num; i++) {
pthread_join(pool->threads[i], NULL);
}
threadpool_free(pool);

return 0;
}

int threadpool_free(threadpool_t *pool)
{
if (pool == NULL) {
return -1;
}

if (pool->task_queue) {
free(pool->task_queue);
}
if (pool->threads) {
free(pool->threads);
pthread_mutex_lock(&(pool->lock));
pthread_mutex_destroy(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
pthread_mutex_destroy(&(pool->thread_counter));
pthread_cond_destroy(&(pool->queue_not_empty));
pthread_cond_destroy(&(pool->queue_not_full));
}
free(pool);
pool = NULL;

return 0;
}

int threadpool_all_threadnum(threadpool_t *pool)
{
int all_threadnum = -1; // 总线程数

pthread_mutex_lock(&(pool->lock));
all_threadnum = pool->live_thr_num; // 存活线程数
pthread_mutex_unlock(&(pool->lock));

return all_threadnum;
}

int threadpool_busy_threadnum(threadpool_t *pool)
{
int busy_threadnum = -1; // 忙线程数

pthread_mutex_lock(&(pool->thread_counter));
busy_threadnum = pool->busy_thr_num;
pthread_mutex_unlock(&(pool->thread_counter));

return busy_threadnum;
}

int is_thread_alive(pthread_t tid)
{
int kill_rc = pthread_kill(tid, 0); //发0号信号,测试线程是否存活
if (kill_rc == ESRCH) {
return false;
}
return true;
}

/*测试*/

#if 1

/* 线程池中的线程,模拟处理业务 */
void *process(void *arg)
{
printf("thread 0x%x working on task %d\n ",(unsigned int)pthread_self(),(int)arg);
sleep(1); //模拟 小---大写
printf("task %d is end\n",(int)arg);

return NULL;
}

int main(void)
{
/*threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size);*/

threadpool_t *thp = threadpool_create(3,100,100); /*创建线程池,池里最小3个线程,最大100,队列最大100*/
printf("pool inited");

//int *num = (int *)malloc(sizeof(int)*20);
int num[20], i;
for (i = 0; i < 20; i++) {
num[i] = i;
printf("add task %d\n",i);

/*int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg) */

threadpool_add(thp, process, (void*)&num[i]); /* 向线程池中添加任务 */
}

sleep(10); /* 等子线程完成任务 */
threadpool_destroy(thp);

return 0;
}

#endif

好再空过去看了也记不住

92-98

TCP通信和UDP通信各自的优缺点:

1
2
3
4
5
6
7
8
9
TCP:	面向连接的,可靠数据包传输。对于不稳定的网络层,采取完全弥补的通信方式。 丢包重传。
优点:
稳定。
数据流量稳定、速度稳定、顺序也不变
电脑A通过TCP协议与电脑B进行数据通信时,在第一次进行通信时通过路由寻路,在互联网中探索出一条通路,如果网络环境不发生变化,这条路之后就不变了,数据包都按照这个路径发送
缺点:
传输速度慢。效率低。开销大。
使用场景:数据的完整型要求较高,不追求效率。
典型的大数据传输、文件传输。比如迅雷可以断点续传,(注意网络下载虽然不支持网络传输,但是它是TCP传输的,只是没有保存上一次的路径)

​ 一般中小型企业选择TCP,在数据完整性的基础上,只需要增大服务器的处理能力就能达到需求,但是TCP协议用到极致也不会提高多少,所以大型企业还是用UDP进行通讯
​ ![1708852255296](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1708852255296.png)

UDP:	无连接的,不可靠的数据报传递。对于不稳定的网络层,采取完全不弥补的通信方式。 默认还原网络状况


​ 优点:

传输速度块。效率高。开销小。

    缺点:
        不稳定。
            数据流量。速度。顺序。
            UDP在发送时会根据网络状况动态的去选择路由节点进行巡逻,不同的包发送路径不同


    使用场景:对时效性要求较高场合。稳定性其次。

          游戏、视频会议、视频电话。		腾讯、华为、阿里  ---  应用层数据校验协议,弥补udp的不足。

UDP通信

![1708852255296](D:\Program Files (x86)\MyBlog\source_posts\网络编程\图片4.png)

UDP实现的 C/S 模型:

recv()/send() 只能用于 TCP 通信。 替代 read、write

accpet(); ---- Connect(); ---被舍弃

server:

    lfd = socket(AF_INET, STREAM, 0);	SOCK_DGRAM --- 报式协议。

    bind();

    listen();  --- 可有可无

    while(1){

        read(cfd, buf, sizeof) --- 被替换 --- recvfrom() --- 涵盖accept传出地址结构。

            ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);

                sockfd: 套接字

                buf:缓冲区地址

                len:缓冲区大小

                flags: 0

                src_addr:(struct sockaddr *)&addr 传出。 对端地址结构

                addrlen:传入传出。

            返回值: 成功接收数据字节数。 失败:-1 errn。 0: 对端关闭。

        小-- 大
        write();--- 被替换 --- sendto()---- connect

                 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

                    sockfd: 套接字

                    buf:存储数据的缓冲区

                    len:数据长度

                    flags: 0

                    src_addr:(struct sockaddr *)&addr 传入。 目标地址结构

                    addrlen:地址结构长度。

                返回值:成功写出数据字节数。 失败 -1, errno		
        }

        close();
    client:

        connfd = socket(AF_INET, SOCK_DGRAM, 0);

        sendto(‘服务器的地址结构’, 地址结构大小)

        recvfrom()

        写到屏幕

        close();

服务器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <string.h>  
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>

#define SERV_PORT 8000

int main(void)
{
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
int sockfd;
char buf[BUFSIZ];
char str[INET_ADDRSTRLEN];
int i, n;

sockfd = socket(AF_INET, SOCK_DGRAM, 0);

bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_PORT);

bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

printf("Accepting connections ...\n");
while (1) {
clie_addr_len = sizeof(clie_addr);
n = recvfrom(sockfd, buf, BUFSIZ,0, (struct sockaddr *)&clie_addr, &clie_addr_len);
if (n == -1)
perror("recvfrom error");

printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));

for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);

n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&clie_addr, sizeof(clie_addr));
if (n == -1)
perror("sendto error");
}

close(sockfd);

return 0;
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#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;
}

本地套接字和网络套接字

1527923816604

IPC通信:

管道pipe:易用性最强

有名管道fifo:可以在没有血缘关系的进程之间进行通信

共享内存映射mmap:可以在没有血缘关系的进程之间进行通信并且可以反复读取

信号:开销最小的

本地套接字(domain):稳定性最好 ——是用CS模型实现本地套接字来实现进程间通讯

本地套接字对比网络编程 TCP C/S模型,就是服务器和客户端分别生成一个套接字文件,相互连接进行通讯。这个套接字文件是伪文件,它不占用磁盘大小,是通过调用bind函数创建出来的

网络通信

  1. 多进程:也就是为每个客户端分配一个进程来处理请求(记得回收子进程)

img

  1. 多线程:当服务器与客户端 TCP 完成连接后,通过 pthread_create() 函数创建线程,然后将「已连接 Socket」的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。

为每个请求分配一个进程/线程的方式不合适,那有没有可能只使用一个进程来维护多个 Socket 呢?答案是有的,那就是 I/O 多路复用技术:

  1. select/poll:存在缺点–当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。
  2. epoll:很好的解决了 select/poll 的问题
  • epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。
  • epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。

Libevent库

本地套接字对比网络编程 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. bind() 地址结构:  sockaddr_in --> sockaddr_un

    struct sockaddr_in srv_addr; --> struct sockaddr_un srv_adrr;

    srv_addr.sin_family = AF_INET;  --> srv_addr.sun_family = AF_UNIX;
    srv_addr.sin_port = htons(8888);    strcpy(srv_addr.sun_path, "srv.socket") 
    由于不需要网络通信了,也不需要ip地址和端口号,这是在本地生成一个套接字,名称是 "srv.socket",位置在srv_addr.sun_path是默认的
    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); 
    此时结构体的大小不再用sizeof计算,len = offsetof(struct sockaddr_un, sun_path) + strlen("srv.socket");

由于sock_addr数据结构:现在用的是第三个sockaddr_un,求大小的时候是加上地址便宜,后面的108字节路径名就是”srv.socket”。

​ ![1708852255296](D:\Program Files (x86)\MyBlog\source_posts\网络编程\图片5.png)

3. bind()函数调用成功,会创建一个 socket。因此为保证bind成功,通常我们在 bind之前, 可以使用 unlink("srv.socket");  这个函数是将个文件的硬连接计数-1,是为了能够顺利创建"srv.socket"文件,就先删一下"srv.socket"文件保证创建成功


4. 客户端不能依赖 “隐式绑定”。并且应该在通信建立过程中,创建且初始化2个地址结构:

    1) client_addr --> bind()

    2)  server_addr --> connect();
    (这两个地址结构分别根据之前定义的两个socket名绑定对应的文件)

实现代码

服务器代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#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;
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#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;
}

对比本地套接字和网络套接字

![1709000247505](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1709000247505.png)

​ ![1709000229274](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1709000229274.png)

libevent库

开源。精简。跨平台(Windows、Linux、maxos、unix)。专注于网络通信。

源码包安装:

参考 README、readme

./configure		检查安装环境 生成 makefile

make			生成 .o 和 可执行文件

sudo make install	将必要的资源cp至系统指定目录。

进入 sample 目录,运行demo验证库安装使用情况。

编译使用库的 .c 时,需要加 -levent 选项。不然会出现未定义的引用,因此需要建立软连接 将缺的那个库添加到默认库的文件夹下。建立动态库应该是libevent.so,静态库应该是libevent.a,在连接的时候需要指定库名,去掉lib前缀,去掉.so后缀 event

库名 libevent.so --> /usr/local/lib   查看的到。

特性:

​ 基于“事件”异步通信模型。— 回调。

事件就是这个库名event,它在里边把所有的东西全部封装成事件(struct event event_new();event_init() ),所见皆事件。之前说的linux系统是所有东西都是文件,所见皆文件

同步/异步通信模型

同步通信模型:有时效性的,并行的去访问,为了防止数据错乱需要安排先后顺序

异步通信模型:异步通信主要依赖回调机制。

​ 函数的编写时机和调用时机不是同一时间的,当封装好了一个函数放在这里,这个函数不是 我封装事件或者代码满足条件的时候(阻塞或者if语句?)去执行,只有在某个条件到达的时 候才会执行这个代码。这个代码是内核调用的。

类似于信号捕捉,只要把函数注册上,只要条件满足了这个回调函数就自动被调用了,函数注册的时间和函数调用的时间不是一个时间

函数指针

未决和非未决:

非未决: 没有资格被处理(前期准备条件都已经准备完了,就等着事件被触发

未决: 有资格被处理,但尚未被处理(还没有准备好,即使事件被触发了也没有被触发的机会

event_new --> event ---> 非未决 --> event_add --> 未决 --> dispatch() && 监听事件被触发 --> 激活态 

--> 执行回调函数 --> 处理态 --> 非未决 event_add && EV_PERSIST --> 未决 --> event_del --> 非未决

![1709035249826](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1709035249826.png)

带缓冲区的事件 bufferevent

#include <event2/bufferevent.h> 

read/write 两个缓冲. 借助 队列.

原理:bufferevent有两个缓冲区∶也是队列实现,读走没,先进先出。
读︰有数据-->读回调函数被调用-->使用bufferevent_read() -->读数据。
写∶使用bufferevent_write() -->向写缓冲中写数据-->该缓冲区有数据自动写出-->写完,回调函数被调用(鸡肋)。

buf是一个结构体

![1709036653944](D:\Program Files (x86)\MyBlog\source_posts\网络编程\1709036653944.png)

bufferevent相关函数

启动和关闭缓冲区

因为套接字里面要求缓冲区可以双向全双工和半关闭

而bufferevent打造出来的缓冲区相当于fd(套接字)也封装到它的内部,它的read和write也支持半关闭

web大练习

借助浏览器和自己电脑上的服务器完成一些通信

自己的服务器上的一个文件夹里有一些文件,可以从浏览器上通过域名解析得到ip(公网ip)地址和端口号进行访问,但是公网ip要申请,所以还是用局域网,127.0.0.1这个

准备工作:html语言

前端,搭建网页

http超文本传输协议

请求协议

![1709036653944](D:\Program Files (x86)\MyBlog\source_posts\网络编程\图片6.png)

应答协议

![1709036653944](D:\Program Files (x86)\MyBlog\source_posts\网络编程\图片7.png)

实现思路:

与高并发服务器类似

只是在得到cfd之后的read阶段,read的内容是http请求头,而且对象也是一个文件描述符,如果读一行对象是文件的话可以用fget()函数,但是这是文件描述符,我们根据read()函数自己写了封装了一个readline()函数,但是他默认的是在linux系统下读取文件描述符对应的文件,这个文件以\n结尾,但是http是\r\n