0%

使用udp打洞实现内网穿透

做项目的时候发现需要进行udp打洞,这里进行简单的验证

因为是测试,代码就先写在一个cpp文件里了,最后再封装。

github项目连接:Chen-Jin-yuan/UDP-hole-punching (github.com)


日志

公网环境:ubuntu 20.04 LTS,g++

本地环境:win10,vs2022,网络利用nattypetest测试cone类型。

  • 首先进行udp收发测试
  • 初次尝试udp打洞失败,思考可能与NAT类型有关,因为校园网和手机热点都是对称型,难以穿透;而如果两台内网机器连同一个wifi的话,可能会因为NAT回环而被丢弃,但是又没有其他网络可以连,错误的可能很多,先尝试逆向连接。
  • 进行逆向连接测试,成功。
  • 进行NAT回环测试(客户端A、B在同一个内网下,A向NAT发往B的包被丢弃),成功,并修改服务器逻辑,当双方在一个NAT内时,发的是主机ip(私有ip)和绑定的端口(不是NAT中的端口)。
  • 逆向连接成功后进行tcp连接,失败,与conntrack连接跟踪有关,可能被防火墙根据协议号阻拦了。
  • 根据连接跟踪,使用tcp打洞打穿NAT和防火墙,实现tcp的逆向连接。
  • tcp的内网穿透失败,防火墙有诸多限制。
  • udp打洞测试成功,成功内网穿透了两个NAT。
  • 整理代码与封装,并有使用示例。

简单的收发测试

Linux服务器端server.cpp

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
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
using namespace std;

void init_udp_Socket(int& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//UDP
if(listenfd < 0)
{
printf("create listen socket error, port-%d\n",port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//端口复用,在bind前设置
int optval = 1;
int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int));
if(ret == -1)
{
close(listenfd);
exit(1);
}

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if(bind(listenfd,(struct sockaddr *)&socketaddr,sizeof(socketaddr))==-1)
{
close(listenfd);
exit(1);
}
}

string udp_hole_punching(int listenfd)
{
//定义sockaddr_in
struct sockaddr_in gateway;//表示网关
socklen_t addr_len = sizeof(gateway);
memset(&gateway, 0, sizeof(gateway));
char recvbuf[128];

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(listenfd, recvbuf, 128, 0, (struct sockaddr *)&gateway, &addr_len);
if(res < 0)
{
printf("udp hole punching receive error!\n");
return "";
}
else
{
string ip = string(inet_ntoa(gateway.sin_addr));
string port = to_string(ntohs(gateway.sin_port));
//区分client1和client2
if(strcmp(recvbuf,"client1")==0)
{
printf("udp hole punching client1 ip: %s, port: %s\n",ip.c_str(),port.c_str());
return ip+" "+port;
}
else if(strcmp(recvbuf,"client2")==0)
{
printf("udp hole punching client2 ip: %s, port: %s\n",ip.c_str(),port.c_str());
return ip+" "+port;
}
}
return "";
}

int main()
{
int listenudp;
const int port = 10000;
init_udp_Socket(listenudp , port);
string ip_port1 = udp_hole_punching(listenudp);
string ip_port2 = udp_hole_punching(listenudp);
if(ip_port1 == "" || ip_port2 == "")
exit(1);

size_t pos1 = ip_port1.find(" ");
string ip1 = ip_port1.substr(0,pos1);
string port1 = ip_port1.substr(pos1+1);

//定义sockaddr_in
struct sockaddr_in socketaddr1;//告知要发送的目标ip及端口
socketaddr1.sin_family = AF_INET;//ipv4
socketaddr1.sin_port = htons(stoi(port1));//字节序转换
inet_pton(AF_INET, ip1.c_str(), &socketaddr1.sin_addr);




size_t pos2 = ip_port2.find(" ");
string ip2 = ip_port2.substr(0,pos2);
string port2 = ip_port2.substr(pos2+1);

//定义sockaddr_in
struct sockaddr_in socketaddr2;//告知要发送的目标ip及端口
socketaddr2.sin_family = AF_INET;//ipv4
socketaddr2.sin_port = htons(stoi(port2));//字节序转换
inet_pton(AF_INET, ip2.c_str(), &socketaddr2.sin_addr);

//对着发,ipport1发给addr2
int res = sendto(listenudp, ip_port1.c_str(),
ip_port1.size(), 0, (struct sockaddr*)&socketaddr2, sizeof(socketaddr2));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

res = sendto(listenudp, ip_port2.c_str(),
ip_port2.size(), 0, (struct sockaddr*)&socketaddr1, sizeof(socketaddr1));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

return 0;
}

windows客户端client.cpp

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
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,
recvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
char sendbuf[10] = "client1";//client1或client2
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}
int main()
{
initSocket();
SOCKET udpfd;
const int port = 10000;//同一台机器的话要改一下
init_udp_Socket(udpfd, port);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 1 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

getchar();//阻塞
closesocket(udpfd);
WSACleanup();
return 0;
}

结果

云服务器里运行server.cpp

本机使用vs2022,先release一份exe文件,然后改端口和client1还是client2的信息,在debug模式再运行一个程序,就可以做到两台主机了。

image-20221030221930721

使用另一台电脑运行程序,绑定端口10000,另一台电脑连不同的网,结果如下:

image-20221030223259010


现在产生p2p发送,发送端添加如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
getchar();//阻塞

//下面向对方发信息
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello";
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}

closesocket(udpfd);

接收端把getchar去掉,添加如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//client2接收信息
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}

printf("recv: %s", recvbuf);
getchar();//阻塞

结果是发不过去,接收端没有收到一直在阻塞。这需要进一步去思考。可能是NAT类型问题,也可能是NAT回环问题(注:经后面测试,发现是NAT回环问题)

逆向连接-Connection reversal

这种方法在两个端点中有一个不存在中间件(如NAT)的时候有效。例如,Client A在NAT之后而Client B拥有全局IP地址,如图所示:

img

Client A内网地址为10.0.0.1,使用TCP,端口为1234。A和Server S建立了一个连接,Server的IP地址为18.181.0.31,监听1235端口。NAT A给Client A分配了TCP端口62000,地址为NAT的公网IP地址155.99.25.11,作为Client A对外当前会话的临时IP和端口。因此Server S认为Client A就是155.99.25.11:62000。而Client B由于有公网地址,所以对Server S来说Client B就是138.76.29.7:1234。

当Client B想要主动发起对Client A的P2P连接时,需要指定目的地址及端口为155.99.25.11:62000。由于NAT工作的原理问题,NAT A会拒绝将收到的对Client A的请求转发给Client A。拒绝该请求主要有如下原因:

  1. NAT A没有映射过62000端口,NAT A不知道该请求是给谁的

  2. NAT A映射过62000端口,但是需要首先从Client A发起请求,然后才能转发应答(限制锥形NAT的保护)

在直接连接Client A失败之后,Client B可以通过Server S向Client A中继一个连接请求,从而从Client A方向“逆向“地建立起Client A- Client B之间的点对点连接(因为Client A连接到了Server S)。

很多当前的P2P系统都实现了这种技术,但其局限性也是很明显的,只有当其中一方有公网IP时连接才能建立。越来越多的情况下,通信的双方都在NAT之后,因此就要用到打洞技术了。


现在我们假设公网服务器有一个clientB,本地主机clientA在内网里,我们的目标是实现B发送udp请求能连接到A。

  • 首先通过服务器分发对方的IP和port
  • 然后clientA向clientB发送信息(正常情况下能收到),此时NAT A已经建立了NAT A->clientB的映射,表示信任B
  • 现在B可以先A发送信息了,NAT A会转发到A

注意B是否接收A的消息是无关紧要,为了验证,这里本地客户端B接收并打印出来。实际上不需要接收,A发的这个包只是告诉NAT A:“我要发的目的ip和端口是被我信任的,它可以发包给我,不要丢弃这个ip和端口发来的信息”。

先写一个在公网云服务器运行的clientB,先接收然后发送:

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
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
#include "time.h"
using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;

void init_udp_Socket(int& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
close(listenfd);
exit(1);
}
}

bool udp_hole_punching(int& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
char sendbuf[10] = "client2";//client1或client2
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res < 0 )
{
printf("udp sendto error!\n");
return false;
}
return true;

}
int main()
{
int udpfd;
const int port = 22222;
init_udp_Socket(udpfd, port);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
close(udpfd);
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 2 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

//getchar();//阻塞
//先接收A发送的消息
printf("recv...\n");
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
return 0;
}
printf("recv: %s\n",recvbuf);
//发送消息,一直发
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello";

while (1)
{
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res < 0)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");
sleep(2);
}
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
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
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,r
ecvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
char sendbuf[10] = "client1";//client1或client2
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}
int main()
{
initSocket();
SOCKET udpfd;
const int port = 22222;
init_udp_Socket(udpfd, port);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 1 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

Sleep(2000);

//下面向对方发信息
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello!!";
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");


while (1)
{
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}

printf("recv: %s\n", recvbuf);
Sleep(2000);

}

closesocket(udpfd);
WSACleanup();
return 0;
}

结果

如图,公网clientB成功发送udp包到内网clientA

image-20221031125624691


现在,我们不让A发送给B,B也不阻塞接收,而是一直发送信息给A。

注意,必须等NAT A中的映射自然删除(大概几分钟的存活时间),没删除前B仍然可以直接发给A。

删除后,B就无法发送给A了,A一直在阻塞,因为B发送的消息给NAT A丢弃了,即保护了A。

image-20221031130440814

NAT回环(环回)

现在我们处理同一个局域网的问题,有些NAT对同一个局域网的信息会丢弃,这时服务器根据ip判断是否是一个局域网,是的话把客户端本机的ip和port发送回去即可。

客户端:

下面是一个获取本机ip的函数,一般取最后一个即可,然后把ip和端口发给服务器。

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
#pragma warning(disable: 4996)
vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//获取主机名
//根据主机名获取主机信息列表,有多个ip(网卡)
hostent* host = gethostbyname(name);//需要禁用c4996警报

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;//转型

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
//cout << addr << endl;
result.push_back(addr);
}

return result;
}

发送包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1];//取最后一个
string myip_port = myip +" "+to_string(myport);
//现在发送信息是本机ip
int res = sendto(udpfd, myip_port.c_str(),myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}

服务器端:

服务器不再接收”client1”或”client2”,而是接收客户端本机的ip和port,接收后判断NAT的ip是否相同,如果相同就是同一个局域网。

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
string udp_hole_punching(int listenfd)
{
//定义sockaddr_in
struct sockaddr_in gateway;//表示网关
socklen_t addr_len = sizeof(gateway);
memset(&gateway, 0, sizeof(gateway));
char recvbuf[128];

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(listenfd, recvbuf, 128, 0, (struct sockaddr *)&gateway, &addr_len);
if(res < 0)
{
printf("udp hole punching receive error!\n");
return "";
}
else
{
string ip = string(inet_ntoa(gateway.sin_addr));
string port = to_string(ntohs(gateway.sin_port));

string host = string(recvbuf);
printf("udp hole punching NAT ip: %s, port: %s; host:%s\n",ip.c_str(),port.c_str(),host.c_str());

return ip+" "+port+" "+host;
}
return "";
}

由于有空格分隔,这里用个解析函数解析两个ip-port

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vector<string> parse(string str)//解析函数
{
str = str + " ";//最后补个空格
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = str.find(' ', pos)) != string::npos)
{
res.push_back(str.substr(pos, pos1 - pos));
while (str[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}
return res;//返回值是右值,外部vector会接收右值,调用移动构造
}

最后在main里:

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
string ip_port1 = udp_hole_punching(listenudp);
string ip_port2 = udp_hole_punching(listenudp);
if(ip_port1 == "" || ip_port2 == "")
exit(1);

vector<string> host1 = parse(ip_port1);
vector<string> host2 = parse(ip_port2);

//服务器发回去的还是使用NAT ip
string ip1 = host1[0];
string port1 = host1[1];
string ip2 = host2[0];
string port2 = host2[1];

//定义sockaddr_in
struct sockaddr_in socketaddr1;//告知要发送的目标ip及端口
socketaddr1.sin_family = AF_INET;//ipv4
socketaddr1.sin_port = htons(stoi(port1));//字节序转换
inet_pton(AF_INET, ip1.c_str(), &socketaddr1.sin_addr);

//定义sockaddr_in
struct sockaddr_in socketaddr2;//告知要发送的目标ip及端口
socketaddr2.sin_family = AF_INET;//ipv4
socketaddr2.sin_port = htons(stoi(port2));//字节序转换
inet_pton(AF_INET, ip2.c_str(), &socketaddr2.sin_addr);

//但是发送的数据不一样了
//对着发,ipport1发给addr2
if(ip1 != ip2)//不同的内网
{
ip_port1 = ip1 + " " + port1;
ip_port2 = ip2 + " " + port2;
}
else//在同一个NAT里
{
ip_port1 = host1[2] + " " + host1[3];
ip_port2 = host2[2] + " " + host2[3];
}

int res = sendto(listenudp, ip_port1.c_str(),
ip_port1.size(), 0, (struct sockaddr*)&socketaddr2, sizeof(socketaddr2));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

res = sendto(listenudp, ip_port2.c_str(),
ip_port2.size(), 0, (struct sockaddr*)&socketaddr1, sizeof(socketaddr1));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

结果

在主机上跑两个程序,像逆向连接一样,A先B发送一个包,然后B就可以一直向A发包了。

当然这里服务器判断两个主机都在内网里,所以返回它们本地的ip和各自绑定的端口(这个端口不是NAT映射中的端口),此时这两个主机能互相发包。

image-20221031150918382

下面修改服务器代码,不管怎么样都返回NAT的ip和port,这时客户端A发给NAT发现目标就是自己内网里的机器,就直接丢弃了,因此B不会收到A的hello!

这个例子就说明了有些NAT不支持回环。

image-20221031151655791

完整代码

完整的源码再放一下:

服务器端:

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
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
#include <vector>

using namespace std;

void init_udp_Socket(int& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//UDP
if(listenfd < 0)
{
printf("create listen socket error, port-%d\n",port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//端口复用,在bind前设置,否则bind时出错就晚了
int optval = 1;
int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int));
if(ret == -1)
{
close(listenfd);
exit(1);
}

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if(bind(listenfd,(struct sockaddr *)&socketaddr,sizeof(socketaddr))==-1)
{
close(listenfd);
exit(1);
}
//完事了
}

string udp_hole_punching(int listenfd)
{
//定义sockaddr_in
struct sockaddr_in gateway;//表示网关
socklen_t addr_len = sizeof(gateway);
memset(&gateway, 0, sizeof(gateway));
char recvbuf[128];
memset(&recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(listenfd, recvbuf, 128, 0, (struct sockaddr *)&gateway, &addr_len);
if(res < 0)
{
printf("udp hole punching receive error!\n");
return "";
}
else
{
string ip = string(inet_ntoa(gateway.sin_addr));
string port = to_string(ntohs(gateway.sin_port));

string host = string(recvbuf);
printf("udp hole punching NAT ip: %s, port: %s; host:%s\n",ip.c_str(),port.c_str(),host.c_str());

return ip+" "+port+" "+host;
}
return "";
}

vector<string> parse(string str)//解析函数
{
str = str + " ";//最后补个空格
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = str.find(' ', pos)) != string::npos)
{
res.push_back(str.substr(pos, pos1 - pos));
while (str[pos1] == ' ')//过滤空格
pos1++;
pos = pos1;
}
return res;//返回值是右值,外部vector会接收右值,调用移动构造
}

int main()
{
int listenudp;
const int port = 10000;
init_udp_Socket(listenudp , port);
string ip_port1 = udp_hole_punching(listenudp);
string ip_port2 = udp_hole_punching(listenudp);
if(ip_port1 == "" || ip_port2 == "")
exit(1);

vector<string> host1 = parse(ip_port1);
vector<string> host2 = parse(ip_port2);

//服务器发回去的还是使用NAT ip
string ip1 = host1[0];
string port1 = host1[1];
string ip2 = host2[0];
string port2 = host2[1];

//定义sockaddr_in
struct sockaddr_in socketaddr1;//告知要发送的目标ip及端口
socketaddr1.sin_family = AF_INET;//ipv4
socketaddr1.sin_port = htons(stoi(port1));//字节序转换
inet_pton(AF_INET, ip1.c_str(), &socketaddr1.sin_addr);

//定义sockaddr_in
struct sockaddr_in socketaddr2;//告知要发送的目标ip及端口
socketaddr2.sin_family = AF_INET;//ipv4
socketaddr2.sin_port = htons(stoi(port2));//字节序转换
inet_pton(AF_INET, ip2.c_str(), &socketaddr2.sin_addr);

//但是发送的数据不一样了
//对着发,ipport1发给addr2
if(ip1 != ip2)//不同的内网
{
ip_port1 = ip1 + " " + port1;
ip_port2 = ip2 + " " + port2;
}
else//在同一个NAT里
{
ip_port1 = host1[2] + " " + host1[3];
ip_port2 = host2[2] + " " + host2[3];
}

int res = sendto(listenudp, ip_port1.c_str(),
ip_port1.size(), 0, (struct sockaddr*)&socketaddr2, sizeof(socketaddr2));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

res = sendto(listenudp, ip_port2.c_str(),
ip_port2.size(), 0, (struct sockaddr*)&socketaddr1, sizeof(socketaddr1));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

return 0;
}

客户端1

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
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#include <vector>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#pragma warning(disable: 4996)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;
const int myport = 22222;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,r
ecvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//获取主机名
//根据主机名获取主机信息列表,有多个ip(网卡)
hostent* host = gethostbyname(name);//需要禁用c4996警报

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;//转型

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
//cout << addr << endl;
result.push_back(addr);
}

return result;
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1];//取最后一个
string myip_port = myip +" "+to_string(myport);
//现在发送信息是本机ip
int res = sendto(udpfd, myip_port.c_str(),myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}



int main()
{
initSocket();

SOCKET udpfd;
init_udp_Socket(udpfd, myport);

if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 1 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

Sleep(2000);

//下面向对方发信息
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello!!";
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");


while (1)
{
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}

printf("recv: %s\n", recvbuf);
Sleep(2000);

}

closesocket(udpfd);
WSACleanup();
return 0;
}

客户端2,相对于客户端1只修改了绑定的port和接收发送的逻辑。

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
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#include <vector>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#pragma warning(disable: 4996)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;
const int myport = 11111;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,r
ecvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//获取主机名
//根据主机名获取主机信息列表,有多个ip(网卡)
hostent* host = gethostbyname(name);//需要禁用c4996警报

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;//转型

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
//cout << addr << endl;
result.push_back(addr);
}

return result;
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1];//取最后一个
string myip_port = myip + " " + to_string(myport);
//现在发送信息是本机ip
int res = sendto(udpfd, myip_port.c_str(), myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}



int main()
{
initSocket();

SOCKET udpfd;
init_udp_Socket(udpfd, myport);

if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 2 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());


//发送消息,一直发
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello";

while (1)
{
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res < 0)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");
Sleep(2000);
}

closesocket(udpfd);
WSACleanup();
return 0;
}

逆向连接后进行tcp连接

因为还没有找到合适的网络来测试udp的内网穿透(大多都是校园网,对称型无法处理),所以先等几天,然后开始测试如何逆向连接后,进行p2p的tcp连接。

目前的想法是,当逆向连接完成后,将原来的udp套接字关闭,创建tcp套接字,然后绑定相同的端口(因为NAT记录的端口)。

  • 首先公网客户端接收内网客户端的udp信息,逆向连接完成(可以不接收,内网客户端发送即可,为了验证这里还是接收)
  • 公网客户端发个udp给内网客户端,作为验证
  • 接着两个客户端创建tcp套接字并且都绑定原来的端口(udp套接字可以不关闭,tcp和udp可以共用端口,所以可同时初始化)
  • 内网客户端进入listen状态,阻塞在accept
  • 公网客户端connect内网客户端,tcp连接建立

公网客户端:

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
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
#include "time.h"
using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;

void init_udp_Socket(int& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
close(listenfd);
exit(1);
}
}

bool udp_hole_punching(int& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
char sendbuf[10] = "client2";//client1或client2
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res < 0 )
{
printf("udp sendto error!\n");
return false;
}
return true;

}

void init_tcp_Socket(int& listenfd, const int port)//初始化一个端口
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;


//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
exit(1);
}
}

int main()
{
int udpfd;
int tcpfd;
const int port = 22222;
init_udp_Socket(udpfd, port);
init_tcp_Socket(tcpfd,port);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
close(udpfd);
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 2 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

//getchar();//阻塞
//先接收A发送的消息
printf("recv...\n");
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
return 0;
}
printf("recv: %s\n",recvbuf);

char sendbuf[10] = "hello";

//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);

res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res < 0)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");

if (connect(tcpfd, (struct sockaddr*)&gateway, sizeof(gateway)) == -1)
{
printf("connect fail !\n");
return 0;
}
printf("connect to server successfully!\n");
send(tcpfd, sendbuf, strlen(sendbuf), 0);

close(udpfd);
close(tcpfd);
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
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
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#include <vector>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#pragma warning(disable: 4996)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;
const int myport = 22222;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,r
ecvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//获取主机名
//根据主机名获取主机信息列表,有多个ip(网卡)
hostent* host = gethostbyname(name);//需要禁用c4996警报

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;//转型

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
//cout << addr << endl;
result.push_back(addr);
}

return result;
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1];//取最后一个
string myip_port = myip + " " + to_string(myport);
//现在发送信息是本机ip
int res = sendto(udpfd, myip_port.c_str(), myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}

void init_tcp_Socket(SOCKET& tcpfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
tcpfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(tcpfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
std::cerr << "bind error" << std::endl;
return;
//cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲.
//也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。
}
//开始监听
if (listen(tcpfd, SOMAXCONN) == -1)
{
std::cerr << "listen error" << std::endl;
return;
}
}

int main()
{
initSocket();

SOCKET udpfd;
SOCKET tcpfd;

init_udp_Socket(udpfd, myport);
init_tcp_Socket(tcpfd, myport);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
printf("recv from server...\n");
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 1 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

Sleep(2000);

//下面向对方发信息
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello!!";
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
printf("send...\n");

memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}

printf("recv: %s\n", recvbuf);
//准备接收连接

///客户端套接字
memset(recvbuf, 0, sizeof(recvbuf));

struct sockaddr_in client_addr;//获取客户的地址和端口号,连接后的不分配新端口
socklen_t len = sizeof(client_addr);//socklen_t 相当于 int,但使用int必须强制转型告知编译器
SOCKET conn = accept(tcpfd, (struct sockaddr*)&client_addr, &len);//阻塞,等待连接,成功则创建连接套接字conn描述这个用户
if (conn == -1)
{
std::cerr << "connect error" << std::endl;
closesocket(tcpfd);
return 0;
}
int nRecv = recv(conn, recvbuf, sizeof(recvbuf), 0);
if (nRecv == SOCKET_ERROR)//copy出错
{
std::cerr << "connection to client has been failed" << std::endl;
closesocket(udpfd);
closesocket(tcpfd);
WSACleanup();
return 0;
}


printf("recv: %s\n", recvbuf);
closesocket(udpfd);
closesocket(tcpfd);
WSACleanup();
return 0;
}

结果

连接失败,双方阻塞在connect和accept处,connect的syn包根本到达不了内网机器(反向测试:内网可以connect外网)。为什么udp可以而tcp就不可以了呢?这两个协议的差异就是,我们在发udp之前,先用内网向外网发了udp;而tcp包是直接从外网发到内网的,这可能导致了问题。

连接跟踪

tcp连接失败也许是因为conntrack(连接跟踪),在防火墙、内核(linux)中会跟踪连接,这个连接不是专门指tcp连接。连接跟踪可以让Netfilter(框架)知道某个特定连接的状态,运行连接跟踪的防火墙称作带有状态机制的防火墙,这种防火墙更安全。连接跟踪的信息一般是五元组,当然针对不同协议和特殊修改也有四元组和七元组:

  • 四元组:源IP地址、目的IP地址、源端口、目的端口
  • 五元组:源IP地址、目的IP地址、协议号、源端口、目的端口
  • 七元组:源IP地址、目的IP地址、协议号、源端口、目的端口,服务类型以及接口索引

可以看到,五元组中不仅有ip、端口,还有协议号这一内容,这标识了是tcp还是udp等等。在各种过滤机制下,连接跟踪会发挥作用。

当然我们的内网客户端是Windows下的,Windows下的防火墙是如何写的并不能知道(没开源),但我们可以大概看一下防火墙高级设置:

image-20221101084903251

在入站规则中,可以看到除了地址端口,还有协议。借助连接跟踪的想法,我们猜测:

  • 假如通过udp打洞后,NAT不检测协议,而允许把tcp包转发到主机上,那么主机的防火墙就会把包丢弃。
  • 因为NAT是基于地址端口的,从理论上说端口限制锥形只要报文源、目的地址端口匹配即可;但也只是理论,不排除NAT也会把tcp拦下来

因此可以得出简单的结论:内网穿透时,使用udp打洞后只能打出一条udp的隧道;要打出tcp隧道,就要让某个内网客户端先发tcp的包。也就是说:在基于NAT的情况下,内网穿透只要打穿地址和端口;而在基于防火墙的情况下,内网穿透还要打穿特定协议的隧道


TCP打洞

我们可以重新设计:

  • 首先服务器通知双方端口和ip
  • 不进行udp打洞,直接发tcp包。这里的实现是:双方同时进行connect,利用connect在内核的重传机制(syn是会重传的),第一个tcp包会给对方NAT丢掉(因为没有udp打洞),因为有重传机制不会马上就返回错误。然后对方能进行connect过来(本地防火墙和NAT都允许入站了),本地发现在connect也就建立连接了。同时进行connect建立tcp连接可以参考我为此研究后写的博客:TCP同时打开-深度剖析 | JySama

这里面:

  • 服务器通知双方的NAT地址和端口是必要的,否则对方根本不知道网关的地址;其次,如果不通知端口,对方只能请求到预设端口,但是NAT中端口映射不一定相同,会给NAT丢弃
  • udp、tcp套接字绑定端口也是必要的,这主要是为了udp和tcp端口同步。因为
    • 回环时,服务器发回去的是客户端通告的“我要绑定的端口”,然后双方根据这个通告的端口要连接。
    • 没有回环时,使用的是NAT的端口,这只需要让udp套接字(与服务器交互)和tcp套接字(p2p交互)绑定在一个端口上即可,因为服务器发回的是NAT是建立了udp的端口,如果tcp套接字使用了不同的端口,那么在NAT上就重新建立了一条映射,这时请求udp端口的tcp连接肯定失败了,因为tcp的syn经过服务器发的端口到了udp的套接字。
  • 如果udp和tcp不绑定端口的话,当使用udp时,内核分配一个端口;使用tcp时connect时,又分配一个端口,可能就导致端口不一致了。

关于connect的建立机制,而不用listen-accept,这个还需要更详细地研究。(注:已研究透彻,为此写了一篇博客:TCP同时打开-深度剖析 | JySama

  • 目前是1对1传输文件,不涉及1对多的p2p,互相connect即可。
  • 如果是1对多,那么tcp的一个端口要connect多个对端,只用设置端口可重用即可SO_REUSEADDR,将多个套接字绑定到一个端口。这里不做研究

外网客户端:取消了udp打洞

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
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
#include "time.h"
using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;

void init_udp_Socket(int& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
close(listenfd);
exit(1);
}
}

bool udp_hole_punching(int& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
char sendbuf[10] = "client2";//client1或client2
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res < 0 )
{
printf("udp sendto error!\n");
return false;
}
return true;

}

void init_tcp_Socket(int& listenfd, const int port)//初始化一个端口
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;


//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
exit(1);
}
}

int main()
{
int udpfd;
int tcpfd;
const int port = 22222;
init_udp_Socket(udpfd, port);
init_tcp_Socket(tcpfd,port);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
close(udpfd);
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 2 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());

//getchar();//阻塞
//先接收A发送的消息
printf("recv...\n");
memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res < 0)
{
printf("recv error!\n");
return 0;
}
//printf("recv: %s\n",recvbuf);

char sendbuf[10] = "hello";

//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);

//res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res < 0)
{
printf("udp sendto error!\n");
return false;
}
//printf("send...\n");

if (connect(tcpfd, (struct sockaddr*)&gateway, sizeof(gateway)) == -1)
{
printf("connect fail !\n");
return 0;
}
printf("connect to server successfully!\n");

send(tcpfd, sendbuf, strlen(sendbuf), 0);

close(udpfd);
close(tcpfd);
return 0;
}

内网客户端:取消了udp打洞

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
#include <iostream>
#include <WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton,inet_ntop(获取ip)
#include <chrono>
#include <string>
#include <vector>
#pragma comment(lib,"ws2_32.lib")//链接dll

#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#pragma warning(disable: 4996)

using namespace std;

const char* SERVER_IP = "101.34.2.129";
const int udpPORT3 = 10000;
const int myport = 22222;

void initSocket()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

void init_udp_Socket(SOCKET& listenfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);//UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}
//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;



//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//windows下有bug
/*
在 Windows 中,如果主机 A 使用 UDP 套接字并调用 sendto() 向主机 B 发送内容,
但 B 没有绑定(bind)任何端口,因此 B 不会收到消息,并且然后宿主A调用recvfrom()接收一些消息,r
ecvfrom()会失败,WSAGetLastError()会返回10054。

这是 Windows 的错误。如果UDP socket在发送消息后recv一个ICMP(port unreachable)消息,
这个错误会被存储,下次调用recvfrom()会返回这个错误。
可以使用下面代码禁用错误
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//获取主机名
//根据主机名获取主机信息列表,有多个ip(网卡)
hostent* host = gethostbyname(name);//需要禁用c4996警报

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;//转型

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
//cout << addr << endl;
result.push_back(addr);
}

return result;
}

bool udp_hole_punching(SOCKET& udpfd, const char* server_ip, const int port)//向服务器发送udp
{
//定义sockaddr_in
struct sockaddr_in socketaddr;//告知要发送的目标ip及端口
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

//发送数据,最后的len不用传地址,因为是告知,不用修改
vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1];//取最后一个
string myip_port = myip + " " + to_string(myport);
//现在发送信息是本机ip
int res = sendto(udpfd, myip_port.c_str(), myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}

void init_tcp_Socket(SOCKET& tcpfd, const int port)
{
//定义socketfd,它要绑定监听的网卡地址和端口
tcpfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(tcpfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
std::cerr << "bind error" << std::endl;
return;
//cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲.
//也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。
}

}

int main()
{
initSocket();

SOCKET udpfd;
SOCKET tcpfd;

init_udp_Socket(udpfd, myport);
init_tcp_Socket(tcpfd, myport);
if (!udp_hole_punching(udpfd, SERVER_IP, udpPORT3))
{
closesocket(udpfd);
WSACleanup();
exit(1);
}

//发完就接收信息
//定义sockaddr_in
struct sockaddr_in server;//表示网关
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

//len要传地址,因为要保存写入结构体的长度
printf("recv from server...\n");
int res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}
string ip_port = recvbuf;
size_t pos1 = ip_port.find(" ");
string ip = ip_port.substr(0, pos1);
string portx = ip_port.substr(pos1 + 1);


printf("client 1 recv: ip: %s, port: %s\n", ip.c_str(), portx.c_str());


//下面向对方发信息
//定义sockaddr_in
struct sockaddr_in gateway;//告知要发送的目标ip及端口
gateway.sin_family = AF_INET;//ipv4
gateway.sin_port = htons(stoi(portx));//字节序转换
inet_pton(AF_INET, ip.c_str(), &gateway.sin_addr);


char sendbuf[10] = "hello!!";
//res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
//printf("send...\n");

memset(&server, 0, sizeof(server));
memset(recvbuf, 0, sizeof(recvbuf));
//len要传地址,因为要保存写入结构体的长度
//res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv error %d!\n", WSAGetLastError());
closesocket(udpfd);
WSACleanup();
return 0;
}

//printf("recv: %s\n", recvbuf);
//准备接收连接

if (connect(tcpfd, (struct sockaddr*)&gateway, sizeof(gateway)) == -1)//先发个tcp连接包
{
printf("connect fail !\n");
}

//开始监听
//if (listen(tcpfd, SOMAXCONN) == -1)
//{
// printf("listen error %d!\n", WSAGetLastError());
// return 0;
// }
///客户端套接字

/*
struct sockaddr_in client_addr;//获取客户的地址和端口号,连接后的不分配新端口
socklen_t len = sizeof(client_addr);//socklen_t 相当于 int,但使用int必须强制转型告知编译器
SOCKET conn = accept(tcpfd, (struct sockaddr*)&client_addr, &len);//阻塞,等待连接,成功则创建连接套接字conn描述这个用户
if (conn == -1)
{
std::cerr << "connect error" << std::endl;
closesocket(tcpfd);
return 0;
}*/
memset(recvbuf, 0, sizeof(recvbuf));
int nRecv = recv(tcpfd, recvbuf, sizeof(recvbuf), 0);
if (nRecv == SOCKET_ERROR)//copy出错
{
std::cerr << "connection to client has been failed" << std::endl;
closesocket(udpfd);
closesocket(tcpfd);
WSACleanup();
return 0;
}


printf("recv: %s\n", recvbuf);

closesocket(udpfd);
closesocket(tcpfd);
WSACleanup();
return 0;
}

有udp打洞的tcp连接:

image-20221101000011682

无udp打洞的tcp连接:

image-20221101091010430

补充:已利用udp打洞成功穿透两个NAT

代码用NAT回环的代码即可。对方是另一个大学的寝室网络,接了路由器不是校园网,所以是圆锥型的NAT,可以成功。

image-20221105114055816

代码整理(封装)

客户端

头文件udp_hole_punch.h

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
#ifndef UDP_HOLE_PUNCH_H
#define UDP_HOLE_PUNCH_H

#include <iostream>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <chrono>
#include <string>
#include <vector>
#pragma comment(lib,"ws2_32.lib")

//Using UDP requires disabling errors
#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
#pragma warning(disable: 4996)

using namespace std;

void initSocket(); //init WSA
void initUdpSocket(SOCKET& listenfd, const int port); //init blocking socket
vector<string> getIpList(); //get host private ip

//send udp segment to server
bool notifyServer(SOCKET& udpfd, const char* server_ip, const int serverPort, const int myPort);

//send one or more messages to the other party(punch a hole)
bool udpHolePunch(SOCKET& udpfd, const char* gateway_ip, const int gatewayPort);

//active, receiver
bool udpPunchSide(SOCKET& udpfd, const char* server_ip, const int serverPort , const int myPort);
//passive, sender, return gateway ip and port
pair<string,int> udpPunchedSide(SOCKET& udpfd, const char* server_ip, const int serverPort, const int myPort);

#endif
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
#include "udp_hole_punch.h"

void initSocket()
{
//init WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;

if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cerr << "WSAStartup() error!" << std::endl;
exit(1);
}
}

//blocking socket
void initUdpSocket(SOCKET& listenfd, const int port)
{
listenfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //UDP
if (listenfd < 0)
{
printf("create listen socket error, port-%d\n", port);
exit(1);
}

struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);

//bind port
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
printf("bind port-%d error !\n", port);
closesocket(listenfd);
exit(1);
}

//Using UDP requires disabling errors
/*
* If sending a datagram using the sendto function results in an "ICMP port unreachable" response and the select function is set for readfds,
* the program returns 1 and the subsequent call to the recvfrom function does not work with a WSAECONNRESET (10054) error response.
* In Microsoft Windows NT 4.0, this situation causes the select function to block or time out.
*/
BOOL bEnalbeConnRestError = FALSE;
DWORD dwBytesReturned = 0;
WSAIoctl(listenfd, SIO_UDP_CONNRESET, &bEnalbeConnRestError, sizeof(bEnalbeConnRestError), \
NULL, 0, &dwBytesReturned, NULL, NULL);
}

vector<string> getIpList()
{
vector<string> result;
char name[256];

int getNameRet = gethostname(name, sizeof(name));//get host name
//get private ip list
hostent* host = gethostbyname(name);//need to disable c4996 error

if (NULL == host)
{
return result;
}

in_addr* pAddr = (in_addr*)*host->h_addr_list;

for (int i = 0; host->h_addr_list[i] != 0; i++)
{
char ip[20] = { '\0' };

inet_ntop(AF_INET, &pAddr[i], ip, 16);
string addr = ip;
result.push_back(addr);
}

return result;
}

//send udp segment to server
bool notifyServer(SOCKET& udpfd, const char* server_ip, const int serverPort , const int myPort)
{
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(serverPort);
inet_pton(AF_INET, server_ip, &socketaddr.sin_addr);

vector<string> myipList = getIpList();
string myip = myipList[myipList.size() - 1]; //send the last one
string myip_port = myip + " " + to_string(myPort);
//send the private ip
size_t res = sendto(udpfd, myip_port.c_str(), myip_port.size(), 0, (struct sockaddr*)&socketaddr, sizeof(socketaddr));
if (res == SOCKET_ERROR)
{
printf("udp sendto error!\n");
return false;
}
return true;

}

//send one or more messages to the other party(punch a hole)
bool udpHolePunch(SOCKET& udpfd, const char* gateway_ip, const int gatewayPort)
{
struct sockaddr_in gateway;
gateway.sin_family = AF_INET;
gateway.sin_port = htons(gatewayPort);
inet_pton(AF_INET, gateway_ip, &gateway.sin_addr);


char sendbuf[10] = "hello!";
//send two messages
int res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error1!\n");
return false;
}
res = sendto(udpfd, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&gateway, sizeof(gateway));
if (res == SOCKET_ERROR)
{
printf("udp sendto error2!\n");
return false;
}
return true;
}

bool udpPunchSide(SOCKET& udpfd, const char* server_ip, const int serverPort, const int myPort)
{
initSocket();

initUdpSocket(udpfd, myPort);

//send message to server
if (!notifyServer(udpfd, server_ip, serverPort, myPort))
return false;

//recvfrom server
struct sockaddr_in server;
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

size_t res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv from server error %d!\n", WSAGetLastError());
return false;
}
string gateway_ip_port = recvbuf;
size_t pos1 = gateway_ip_port.find(" ");
string gateway_ip = gateway_ip_port.substr(0, pos1);
string gateway_port = gateway_ip_port.substr(pos1 + 1);


printf("client recv: ip: %s, port: %s\n", gateway_ip.c_str(), gateway_port.c_str()); //debug

if (!udpHolePunch(udpfd, gateway_ip.c_str(), stoi(gateway_port)))
{
printf("udp hole punch error!\n");
return false;
}

return true;
}

pair<string, int> udpPunchedSide(SOCKET& udpfd, const char* server_ip, const int serverPort, const int myPort)
{
initSocket();

initUdpSocket(udpfd, myPort);
pair<string, int> pairRes;
pairRes.first = "";
pairRes.second = -1;
//send message to server
if (!notifyServer(udpfd, server_ip, serverPort, myPort))
return pairRes;

//recvfrom server
struct sockaddr_in server;
socklen_t addr_len = sizeof(server);
memset(&server, 0, sizeof(server));
char recvbuf[128];
memset(recvbuf, 0, sizeof(recvbuf));

size_t res = recvfrom(udpfd, recvbuf, 128, 0, (struct sockaddr*)&server, &addr_len);
if (res == SOCKET_ERROR)
{
printf("recv from server error %d!\n", WSAGetLastError());
return pairRes;
}
string gateway_ip_port = recvbuf;
size_t pos1 = gateway_ip_port.find(" ");
string gateway_ip = gateway_ip_port.substr(0, pos1);
string gateway_port = gateway_ip_port.substr(pos1 + 1);


printf("client recv: ip: %s, port: %s\n", gateway_ip.c_str(), gateway_port.c_str()); //debug
pairRes.first = gateway_ip;
pairRes.second = stoi(gateway_port);
return pairRes;
}

客户端调用示例

main函数调用,接收方主动打洞,发送方不打洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "udp_hole_punch.h"
const char* SERVER_IP = "a.b.c.d"; //your server
const int SERVERUDPPORT = 10000;
const int MYPORT = 22222;
int main()
{
SOCKET udpfd;
if (!udpPunchSide(udpfd, SERVER_IP, SERVERUDPPORT, MYPORT))
{
closesocket(udpfd);
WSACleanup();
return 0;
}
//do other thing...

//end
closesocket(udpfd);
WSACleanup();
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
#include "udp_hole_punch.h"
const char* SERVER_IP = "a.b.c.d"; //your server
const int SERVERUDPPORT = 10000;
const int MYPORT = 22222;
int main()
{
SOCKET udpfd;
pair<string, int> gateway = udpPunchedSide(udpfd, SERVER_IP, SERVERUDPPORT, MYPORT);
if (gateway.first == "")
{
closesocket(udpfd);
WSACleanup();
return 0;
}
const char* gateway_ip = gateway.first.c_str();
const int gateway_port = gateway.second;

//do other thing...

//end
closesocket(udpfd);
WSACleanup();
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
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
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <cstring>
#include <unistd.h>//close
#include <vector>

using namespace std;

void init_udp_Socket(int& listenfd, const int port)
{
listenfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//UDP
if(listenfd < 0)
{
printf("create listen socket error, port-%d\n",port);
exit(1);
}
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(port);
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);

//Port reused
int optval = 1;
int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int));
if(ret == -1)
{
close(listenfd);
exit(1);
}

//bind
if(bind(listenfd,(struct sockaddr *)&socketaddr,sizeof(socketaddr))==-1)
{
close(listenfd);
exit(1);
}
}

string udp_hole_punching(int listenfd)
{
struct sockaddr_in gateway;
socklen_t addr_len = sizeof(gateway);
memset(&gateway, 0, sizeof(gateway));
char recvbuf[128];
memset(&recvbuf, 0, sizeof(recvbuf));

int res = recvfrom(listenfd, recvbuf, 128, 0, (struct sockaddr *)&gateway, &addr_len);
if(res < 0)
{
printf("udp hole punching receive error!\n");
return "";
}
else
{
string ip = string(inet_ntoa(gateway.sin_addr));
string port = to_string(ntohs(gateway.sin_port));

string host = string(recvbuf);
printf("udp hole punching NAT ip: %s, port: %s; host:%s\n",ip.c_str(),port.c_str(),host.c_str());

return ip+" "+port+" "+host;
}
return "";
}

vector<string> parse(string str)
{
str = str + " ";//add a space
vector<string> res;
size_t pos = 0;
size_t pos1;
while ((pos1 = str.find(' ', pos)) != string::npos)
{
res.push_back(str.substr(pos, pos1 - pos));
while (str[pos1] == ' ')
pos1++;
pos = pos1;
}
return res;//move rvalue
}

int main()
{
int listenudp;
const int port = 10000;
init_udp_Socket(listenudp , port);
string ip_port1 = udp_hole_punching(listenudp);
string ip_port2 = udp_hole_punching(listenudp);
if(ip_port1 == "" || ip_port2 == "")
exit(1);

vector<string> host1 = parse(ip_port1);
vector<string> host2 = parse(ip_port2);

//The server sends back using a NAT ip
string ip1 = host1[0];
string port1 = host1[1];
string ip2 = host2[0];
string port2 = host2[1];

struct sockaddr_in socketaddr1;
socketaddr1.sin_family = AF_INET;//ipv4
socketaddr1.sin_port = htons(stoi(port1));
inet_pton(AF_INET, ip1.c_str(), &socketaddr1.sin_addr);

struct sockaddr_in socketaddr2;
socketaddr2.sin_family = AF_INET;//ipv4
socketaddr2.sin_port = htons(stoi(port2));
inet_pton(AF_INET, ip2.c_str(), &socketaddr2.sin_addr);

if(ip1 != ip2) //Different intranets
{
ip_port1 = ip1 + " " + port1;
ip_port2 = ip2 + " " + port2;
}
else //In the same NAT
{
ip_port1 = host1[2] + " " + host1[3];
ip_port2 = host2[2] + " " + host2[3];
}

int res = sendto(listenudp, ip_port1.c_str(),
ip_port1.size(), 0, (struct sockaddr*)&socketaddr2, sizeof(socketaddr2));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

res = sendto(listenudp, ip_port2.c_str(),
ip_port2.size(), 0, (struct sockaddr*)&socketaddr1, sizeof(socketaddr1));
if(res < 0)
{
printf("udp sendto error!\n");
exit(1);
}

return 0;
}

前言

在socket的编程中偶然发现了两端同时connect可以不经过listen-accept而建立tcp连接,然后去找了许多资料,发现这个现象叫同时打开,下面就来具体研究这个连接的建立情况。

同时打开需要去连接对方的端口,因此客户在connect之前需要bind一个端口。

参考资料:

在参考资料的基础上,整理了整个逻辑,并补充了序列号确认的流程和能同步的原因,以及syn丢包时能成功建立的原因。

connect与accept

我从tcp正常的建立说起。

在简单的服务器-客户端模型中,服务器在执行listen()方法之后还会执行一个accept()方法,并阻塞等待客户端连接。客户端在创建socket后可以调用connect()方法来连接服务器。这个过程中,TCP的三次握手究竟什么时候完成呢?

参考资料4中,作者在执行accept前进行sleep等待,让客户端直接去connect并抓包。作者的抓包结果如下:

img

从抓包结果看来,就算不执行accept()方法,三次握手照常进行,并顺利建立连接。


从listen开始说起,在服务器执行listen方法后,就会进入监听(LISTEN)状态,内核会为每个处于监听状态的socket分配两个队列:半连接队列和全连接队列。相信这两个队列大家也不陌生了。

  • 半连接队列(SYN队列),服务端收到第一次握手后,会将socket加入到这个队列中,队列内的socket都处于SYN_RECV 状态。然后发回syn+ack执行第二次握手。
  • 全连接队列(ACCEPT队列),在服务端收到第三次握手后,会将半连接队列的socket取出,放到全连接队列中。队列里的socket都处于 ESTABLISHED状态。这里面的连接,就等着服务端执行accept()后被取出了。

这就是说,accept实际上只是从全连接队列取出一条连接,不参与TCP三次握手的过程。

这两个队列将详细点,全连接队列(icsk_accept_queue)是个链表,而半连接队列(syn_table)是个哈希表

img

这是因为全连接队列只需要把头部的连接给accept即可,维护一个线性结构就可以了。

而半连接队列中的连接都不是完整的连接,在等待第三次握手的到来。当第三次握手到来时,要根据IP端口来看是哪个socket,这就需要哈希表查找。这样,两个队列是时间复杂度都是O(1)。

参考资料4还谈到了这两个队列满了会怎么样,不是本文重点,有兴趣的朋友可以去看看。

  • 全连接队列满了,再来第三次握手也会丢弃,此时如果tcp_abort_on_overflow=1,还会直接发RST给客户端。
  • 半连接队列满了,可能是因为受到了SYN Flood攻击,可以设置tcp_syncookies,绕开半连接队列。

connect是如何保存socket信息的

当服务端回复syn+ack时,客户端实际上也没有处于listen状态的套接字,没有半连接队列和全连接队列,但却可以完成三次握手。这意味着,客户端进行connect调用后,该套接字一定被加入到某个表中,并可以被匹配到。

跟踪内核源码可以看到当调用connect时,对应的套接字就被加入了全局的tcp已连接(established)的表中:

1
2
3
4
5
6
7
8
9
10
11
12
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
...
/* Socket identity is still unknown (sport may be zero).
* However we set state to SYN-SENT and not releasing socket
* lock select source port, enter ourselves into the hash tables and
* complete initialization after this.
*/
tcp_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(&tcp_death_row, sk);
...
}

注意这句注释 enter ourselves into the hash tables,在inet_hash_connect函数中,socket在调用connect的时候就会把自己加入到establish hash表,虽然它此时连syn都还没有发送。确切的讲,应该是在它调用的__inet_check_established函数中。

当内核收到syn+ack报文时,内核是先在established表中查找,再进行listen表的查找。对于客户端来说,syn+ack报文必然可以在已连接表中匹配上对应的套接字。

同时打开

现在进入本文的重点

同时打开连接是指通信的双方在接收到对方的SYN包之前,都进行了主动打开的操作并发出了自己的SYN包。如之前所说一个四元组标识一个TCP连接,因此如果一个TCP连接要同时打开需要通信的双方知晓对方的IP和端口信息才行,这种场景在实际情况中很少发生(NAT穿透中可能会多一些)。同时打开的流程如下图

image.png

状态转移

套接字有以下三种状态:

  • 在发送syn之后状态处于SYN_SENT状态;
  • SYN_RECV状态;
  • ESTABLISHED状态。

在正常的连接过程中,客户端向服务器在监听的端口发起connect,第一次握手发送syn后进入SYN_SENT状态,服务器发回syn+ack之后进入ESTABLISHED状态,服务器处的端口处于SYN_RECV状态,并在第三次握手中发回ack后进入ESTABLISHED状态。

SYN_RECV状态还可以是:处于SYN_SENT状态的套接字接收到syn后会进入的状态。这个过程中,客户发送syn并收到syn,就是一个同时打开的过程(双方同时connect)。

假设两台设备双方均发送syn给对端,理想情况下,在发送syn之后状态处于SYN_SENT状态,此时双方均收到对端的发来的syn,则立即进入SYN_RECV状态,并且都向对端回复syn+ack,在收到syn+ack之后,连接从SYN_RECV状态切换到ESTABLISHED状态。

在发送syn进入SYN_SENT状态之后,收到对端发来的syn包处理流程如下。如果收到syn,进入SYN_RECV状态。如果收到的是ack(syn+ack和ack两种),会进入另外的处理流程,稍后再说。

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
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th)
{

struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
struct tcp_fastopen_cookie foc = { .len = -1 };
int saved_clamp = tp->rx_opt.mss_clamp;
...
if (th->ack) { //包中带ACK标记,走此路径
...//另外的处理流程
return -1;
}
if (th->syn) {
/* We see SYN without ACK. It is attempt of
* simultaneous connect with crossed SYNs.
* Particularly, it can be connect to self.
*/
tcp_set_state(sk, TCP_SYN_RECV);

if (tp->rx_opt.saw_tstamp) {
tp->rx_opt.tstamp_ok = 1;
tcp_store_ts_recent(tp);
tp->tcp_header_len =
sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
} else {
tp->tcp_header_len = sizeof(struct tcphdr);
}

tp->rcv_nxt = TCP_SKB_CB(skb)->seq + 1;
tp->copied_seq = tp->rcv_nxt;
tp->rcv_wup = TCP_SKB_CB(skb)->seq + 1;

/* RFC1323: The window in SYN & SYN/ACK segments is
* never scaled.
*/
tp->snd_wnd = ntohs(th->window);
tp->snd_wl1 = TCP_SKB_CB(skb)->seq;
tp->max_window = tp->snd_wnd;

tcp_ecn_rcv_syn(tp, th);

tcp_mtup_init(sk);
tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
tcp_initialize_rcv_mss(sk);

tcp_send_synack(sk);
}

这个负责发送SYN+ACK的函数tcp_send_synack有些特殊:

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
int tcp_send_synack(struct sock *sk)
{
struct sk_buff *skb;

skb = tcp_write_queue_head(sk);
if (skb == NULL || !(TCP_SKB_CB(skb)->tcp_flags & TCPHDR_SYN)) { //包一定携带SYN标记
pr_debug("%s: wrong queue state\n", __func__);
return -EFAULT;
}
if (!(TCP_SKB_CB(skb)->tcp_flags & TCPHDR_ACK)) { //未携带ACK标记
if (skb_cloned(skb)) {
struct sk_buff *nskb = skb_copy(skb, GFP_ATOMIC);
if (nskb == NULL)
return -ENOMEM;
tcp_unlink_write_queue(skb, sk);
skb_header_release(nskb);
__tcp_add_write_queue_head(sk, nskb);
sk_wmem_free_skb(sk, skb);
sk->sk_wmem_queued += nskb->truesize;
sk_mem_charge(sk, nskb->truesize);
skb = nskb;
}

TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_ACK; //加上ACK标记
TCP_ECN_send_synack(tcp_sk(sk), skb);
}
TCP_SKB_CB(skb)->when = tcp_time_stamp;
return tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC); //发送出去
}

在同时打开的情况下,发送队列中已经有一个SYN包等待确认。tcp_send_synack的基本功能是:将发送队列中的SYN包加上ACK标记位再发送。这样TCP收到SYN后发送的SYN+ACK的序列号与最开始发送的SYN包一致,ack的确认号是对方syn的序列号+1,连接就可以在收到对端的SYN+ACK后得以正常建立。


在SYN_RECV状态收到对端发来的syn+ack包,则直接进入ESTABLISHED已连接状态:

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
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
/* step 5: check the ACK field */
acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH |
FLAG_UPDATE_TS_RECENT) > 0;

switch (sk->sk_state) {
case TCP_SYN_RECV:

/* ack处理失败 */
if (!acceptable)
return 1;

/* RTT */
if (!tp->srtt_us)
tcp_synack_rtt_meas(sk, req);

/* Once we leave TCP_SYN_RECV, we no longer need req
* so release it.
*/
if (req) {
inet_csk(sk)->icsk_retransmits = 0;
reqsk_fastopen_remove(sk, req, false);
} else {
/* Make sure socket is routed, for correct metrics. */
/* 检查重建路由 */
icsk->icsk_af_ops->rebuild_header(sk);
/* 初始化拥塞邋控制 */
tcp_init_congestion_control(sk);
/* 路径mtu发现初始化 */
tcp_mtup_init(sk);
/* 用户待读取数据初始化 */
tp->copied_seq = tp->rcv_nxt;
/* 调整接收发送缓存以及窗口等 */
tcp_init_buffer_space(sk);
}
smp_mb();

/* 连接更新为已连接状态 */
tcp_set_state(sk, TCP_ESTABLISHED);
sk->sk_state_change(sk);
/* 省略了一些代码 */
}

序列号确认

如流程图所示,假设客户端A的syn序列号为x,B的syn序列号为y。双方收到对方的syn时,发送的syn+ack序列号为,A:syn-x+ack-y+1;B:syn-y+ack-x+1

这样,A收到B的syn+ack时,A知道这个是B过来的(syn还是y);并且知道B收到的就是自己的syn(ack确认号是x+1),至此双方序列号就完成了同步。

在这个过程中,有三种情况被其他客户端干扰:

  • 如果客户端C伪造syn-z发给A,A发给B的syn-ack确认号就是z+1,B就知道不对了;
  • 如果客户端C伪造syn-z+ack-n发给了A,A看到syn是z而不是B的y就直接丢弃了。
  • 如果客户端C两个报文都发给A,这样A查看syn+ack时发现syn是z,还要进一步查看ack,发现ack不是自己的x+1,就知道是错误了。

syn丢包

在双方交换四个数据包的过程中,可能会发生丢包。有四种情况:

  • 客户AB向对方发送syn,进入SYN_SEND状态,两个包都丢了,超时重传。

  • 客户AB向对方发送syn,但B的syn包丢了。这时AB都在SYN_SENT状态,B收到A的syn后进入SYN_RECV状态,然后发送syn+ack。
    • A收到syn+ack时还在SYN_SENT状态,注意到源码中有另一条“ack”的处理路径。
    • 实际上客户A在发送syn后收到syn+ack是与listen中的服务器正常建立连接的过程(第二次握手),A并不知道对方是服务器还是客户端,B发的syn-y+ack-x+1中的y在A看来就是服务器的初始序列号。此时A会返回ack并进入ESTABLISHED状态。其中ack的序列号是y+1。
    • B收到ack后发现没有syn,检查ack序列号正确,就知道是自己的syn包丢了,进入ESTABLISHED状态,连接建立完成。
  • 这个过程由于B的syn包丢了,所以实际上变成了client-server模型,退化为正常的三次握手。

  • 客户AB向对方发送syn都收到了,AB都进入SYN_RECV状态,发送syn+ack,如果两个包都丢了,超时重传。

  • 客户AB向对方发送syn都收到了,AB都进入SYN_RECV状态,发送syn+ack。
    • 假设B发送的包丢了,这时B收到syn+ack直接进入ESTABLISHED状态。
    • A一直在等待syn+ack,当超时时,A会猜测是自己的syn丢了(且syn+ack也丢了,因为syn+ack没丢的话就是第二种情况client-server)或是对方的syn+ack丢了。
    • 此时如上所述,A在SYN_RECV状态超时会重传syn+ack,对于A来说,B没发回syn+ack可能有两种情况:
      • 如果B在ESTABLISHED状态,知道自己syn+ack丢了,发回syn+ack让A进入ESTABLISHED状态。
      • 如果B根本就没收到syn(B知道自己发了syn),此时退化为client-server模型,发回ack。

至此,连接建立完成。

同时关闭

同时关闭的流程和四次挥手差不多

image.png

总结

  • 客户端在connect后在内核中会把套接字加入已连接哈希表,收到syn+ack报文先查已连接哈希表再查listen半连接哈希表
  • 客户端同时打开时,双方会进行syn、syn+ack四种报文的发送接收,在三种状态中转移:发送syn的SYN_SENT状态、接收syn发送syn+ack的SYN_RECV状态、接收syn+ack的ESTABLISHED状态。
  • 序列号的同步中,syn发的序列号是随机初始序列号,syn+ack回复的序列号是syn的序列号(标识了该包是自己发的),确认号是对方syn序列号+1(标识了对方的包自己已经收到)。收到syn+ack后会检测序列号(该包是否是发syn的对方发来的)以及确认号(该包是否是自己想连接的发了syn过去的对方发来的)。
  • syn丢包中,如果有一个对方丢了syn包,就退化成client-server模型;如果有一个对方丢了syn+ack包,自己会重传syn+ack,对方要么重传syn+ack(对方的syn+ack丢了),要么重传ack(自己的syn丢了,退化为client-server模型)。

函数模板的声明和定义写在一个头文件里,因为这样在别的cpp(假设是main)里调用(相当于这个cpp就有定义)才能实例化。

否则main-cpp(只有声明没有定义)调用生成的obj文件会去这个头文件(函数模板的声明)对应的obj文件找相应的实例化的函数的二进制代码,但是那个obj文件对应的cpp只有定义而没有调用!而没有调用就没有实例化,所以对应的obj文件也没有实例化函数的二进制代码,这样链接就出事了。


不能在函数模板中打算使用不同的case针对不同的类型进行操作(比如0对int,1对string),因为对于一个类型的函数模板调用就实例化了整个函数,这些case也会被实例在这个类型上,就会出现类型不匹配的错误(错误可不管你case几,因为调用首先要实例化,实例化就发现出错了,没有管参数是多少)。

比如用case0,并且实例化一个int,但是case1中也要根据int生成二进制代码,但人家是string类型,就出错了

这对if也是一样的。

比如这样,这些map对应不同的类型,就会出错。

image-20221022195741403


上面说了case中采用不同的类型,实际上也不能在case中调用不同类型的模板函数。不要用函数模板(假设是1)调用多个函数模板(假设是2)。

比如一个函数模板1里switch调用不同的模板2,你只希望根据case和相应的参数来调用某个模板2,但是由于其他函数模板2也会根据这些参数的类型实例化,然而实例化很可能出错,因为你可能只考虑了某个case,而这些模板2是不同的实现。

比如这样,函数模板2根据map的参数和key的参数推导,但模板1只考虑了某个case和对应的key的类型,这会导致后面的case中出现map和key类型不匹配(因为map的类型1是和key类型共用的)

image-20221022195831467

还是那个问题,实例化考虑参数的类型,执行才考虑参数的值。

最好不要在头文件定义全局变量,很可能会出现重定义,即使使用了ifndef。

因为ifndef只是让预编译时头文件展开不包含重复的头文件,最后每个cpp文件的代码是包含了所有的include了的头文件的代码;

比如A.cpp包含了a.h,和b.h,a.h又包含了b.h,B.cpp包含了a.h;如果在b.h定义了一个全局变量,在编译时不会出错,在链接时就会出错了。

这是因为ifndef只是告诉A.cpp:a.h包含了b.h,不用再包含一次了;而B.cpp包含了ab两个头文件也可以编译成功,这样就cpp文件就生成了两个包含b.h的代码的obj文件,在链接时发现两个obj文件定义了同一个变量,就出现重定义错误了。


因此头文件里最好只声明全局变量(可以成功是因为可能只被include一次),声明必须使用extern T x;,如果不用extern也视为定义只是没有初始化。

但需要在何处定义呢,只需要注意不能在其他头文件定义,这和前面说的是同样的道理。可以在任意的一个cpp文件里再定义一次,注意是定义不是赋值,定义要写上类型。但最好把**.h声明和.cpp定义关联起来**,因此不要随便找个cpp就定义。

其次,直接在cpp定义而不在.h文件声明是可行的,不过这会导致.h文件内依赖该变量的代码编译失败。但这些代码可以放到cpp内而解决这个错误,见仁见智了。


总结:

  • 不要在头文件定义全局变量,声明必须使用extern
  • 定义可以在任意一个cpp文件进行,但这个cpp文件最好和原来的.h文件关联起来,比较直观。

做虚拟化实验需要用物理机上的linux系统,用虚拟机会产生虚拟化嵌套的问题,而学校提供的服务器太烂了(唉(;′⌒`)

因此要安装双系统,这里记录一下。

预处理

  • 要一个u盘
  • 分区
  • 制作系统盘

先看bios模式cmd下msinfo32,可以看到是UEFI(可能会影响后续操作)

image-20221014114454112

首先,进入Windows,对于win10,直接在左下角的搜索栏里搜索磁盘管理(或者右键此电脑->管理->磁盘管理),找到电脑的磁盘划分。开辟一块未分配的新分区:右键->压缩卷。我分了一块50GB的空闲磁盘。然后看一下磁盘的分区形式,这里是GPT

image-20221014133835613

然后我们制作系统u盘,使用rufus软件(官网直接下载Rufus - 轻松创建USB启动盘,界面很好看),选择镜像(先下载好)后注意根据磁盘分区格式选择类型。

image-20221014134113266

启动安装

关机,插入u盘,然后开机,按f9进入bios(我电脑是惠普的,不同电脑不一样)

如果检测到u盘设备可用,就进行下一步;否则,可能是微软的安全设置屏蔽了bios选择,这时按f10进入bios设置,找到安全启动设置(或安全引导等字眼),把它禁用,此时重启会出现提示,让你输验证码,禁用才奏效。然后再次重启基本上就可以检测到u盘设备了。

检测到u盘设备就选择该设备回车,会自动进行引导,等一会。

image-20221014134915951

然后进入ubuntu界面后先选择语言(为了接下来的设置能看懂,可以选中文。如果follow别的教程可以选英文),接着会选择是try ubuntu还是直接install。最好选择try试试能否使用,如果可以使用就选择旁边的install,

image-20221014135113368

最重要的一步是选择磁盘,这里我忘记拍了就用一下别人的图,第一个是系统共存,选最后一个——自己选分区。

img

后面的操作都没拍照,具体参考博客:Windows下采用U盘安装Ubuntu双系统详细过程 | Cola In Library安装Ubuntu部分,主要是设置主分区和逻辑分区,无脑follow。设置完之后就安装就好了。

默认启动设置

我安装后开机是这样的:

image-20221014135723680

默认是ubuntu,10s内不选择自动进ubuntu。这是可以修改的,进入ubuntu,然后修改grub文件。

刚刚启动页面是这样的:

1
2
3
Ubuntu
Advanced options for Ubuntu
Windows 10

那么对应的 GRUB_DEFAULT 顺序是0、1、2

打开文件

1
sudo gedit /etc/default/grub
1
2
3
4
5
6
7
8
9
10
11
# If you change this file, run 'update-grub' afterwards to update
# /boot/grub/grub.cfg.
# For full documentation of the options in this file, see:
# info -f grub -n 'Simple configuration'

GRUB_DEFAULT=2
GRUB_TIMEOUT_STYLE=hidden
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT=""
GRUB_CMDLINE_LINUX="text"

其中GRUB_DEFAULT的值设置的就是默认顺序,改成2就是windows。

GRUB_TIMEOUT是等待时间,可以设置成5。

这样就大功告成了!

前言

接着上一篇博客WebServer模块单元测试 | JySama,写得太多了代码解析就很慢了,重新开一篇。

epoll封装

上一篇末尾给出了epoll的实例,每次添加事件、修改事件、删除事件都是差不多的流程,可以封装一下,并且epollfd的create和close可以变成RAII的管理模式。

因为上层调用时,事件的类型是不一样的(读转读、读转写、写转写等),为了接口易用与通用,让上层传入events描述事件类型。events是一个uinit32_t,也即32位无符号整数类型。

封装很简单,只需要支持添加、修改、删除、调用epoll_wait、设置非阻塞(这个也让上层决定,默认非阻塞)即可。

设置非阻塞

这个功能设置在这里是因为每个要添加进来的事件都可以顺便进行阻塞与非阻塞的设置(而且一般来说事件的模式都是一致的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
阻塞方式是文件读写操作的默认方式,但是应用程序员可通过使用O_NONBLOCK 标志来人为
的设置读写操作为非阻塞方式 .( 该标志定义在 < linux/fcntl.h > 中,在打开文件时指定 ) .

如果设置了 O_NONBLOCK 标志,read 和 write 的行为是不同的 ,如果进程没有数据就绪时调用了 read ,
或者在缓冲区没有空间时调用了 write ,系统只是简单的返回 EAGAIN,而不会阻塞进程.
*/

//fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性
//F_GETFL:获取文件打开方式的标志,标志值含义与open调用一致,然后或上非阻塞标志
//F_SETFL:设置文件打开方式标志为arg指定方式

int setFdNonblock(int fd) {//不判断fd是否小于0,让上层判断
return fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);//出错返回-1
}

事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool addFd(int fd, uint32_t events) {
if(fd < 0) return false;
//如果需要设置非阻塞,根据布尔运算就会调用SetFdNonblock函数,如果返回不是-1就成功,是-1就返回false
if(nonBlock && (setFdNonblock(fd)==-1))
return false;

epoll_event ev = {0};
ev.data.fd = fd;//关联fd
ev.events = events;//上层设置好类型
return 0 == epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &ev);//add,成功返回0
}

bool modFd(int fd, uint32_t events) {
if(fd < 0) return false;
epoll_event ev = {0};
ev.data.fd = fd;
ev.events = events;
return 0 == epoll_ctl(epollFd, EPOLL_CTL_MOD, fd, &ev);//mod
}

bool delFd(int fd) {
if(fd < 0) return false;
return 0 == epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, 0);
}

返回就绪事件

这里上层创建一个events数组把地址传进来接收就绪事件,函数返回值是就绪事件个数。虽然c++定义一个结构体类型可以不写struct(c要),但还是写出来好些

1
2
3
4
5
6
7
8
//对于timeout:-1:永远等待;0:不等待直接返回,执行下面的代码;其他:在超时时间内没有事件发生,返回0,如果有事件发生立即返回
//默认不等待
int wait(vector<struct epoll_event> &evlist, int eventSize, int timeoutMs = 0) {//成功返回多少事件就绪,超时返回0,出错返回-1
struct epoll_event* evs = ;
int res = epoll_wait(epollFd, events, eventSize, timeoutMs);
for(int i=0,i<res;i++)
evlist.push_back(events[i]);
}

完整代码

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
//Epoller.h
#ifndef EPOLLER_H
#define EPOLLER_H

#include <sys/epoll.h> //epoll操作
#include <fcntl.h> // fcntl()
#include <unistd.h> // close()
#include <cassert>
#include <vector>
class Epoller
{
private:
int eventSize;
const bool nonBlock;
int epollFd;

std::vector<struct epoll_event> events;
public:
Epoller(const int eventsize = 1024, const bool ifNonBlock = true):
eventSize(eventsize),nonBlock(ifNonBlock),epollFd(epoll_create(5)),events(eventsize)
{
assert(epollFd>=0);
}
~Epoller()
{
close(epollFd);
}

int setFdNonblock(int fd)
{
return fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);//出错返回-1
}

bool addFd(int fd, uint32_t events)
{
if(fd < 0) return false;
//如果需要设置非阻塞,根据布尔运算就会调用SetFdNonblock函数,如果返回不是-1就成功,是-1就返回false
if(nonBlock && (setFdNonblock(fd)==-1))
return false;

epoll_event ev = {0};
ev.data.fd = fd;//关联fd
ev.events = events;//上层设置好类型
return 0 == epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &ev);//add,成功返回0
}

bool modFd(int fd, uint32_t events)
{
if(fd < 0) return false;
epoll_event ev = {0};
ev.data.fd = fd;
ev.events = events;
return 0 == epoll_ctl(epollFd, EPOLL_CTL_MOD, fd, &ev);//mod
}

bool delFd(int fd)
{
if(fd < 0) return false;
return 0 == epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, 0);
}

//对于timeout:-1:永远等待;0:不等待直接返回,执行下面的代码;其他:在超时时间内没有事件发生,返回0,如果有事件发生立即返回
//默认不等待
int wait(int timeoutMs = 0) //成功返回多少事件就绪,超时返回0,出错返回-1
{
return epoll_wait(epollFd, &events[0], eventSize, timeoutMs);
}
int getEventFd(size_t i)
{
return events[i].data.fd;
}
uint32_t getEvents(size_t i)
{
return events[i].events;
}
};

#endif

前言

看完了webserver的c++11(14)的写法并添加了大量注释后,也要开始自己手写一个了。整体的逻辑还没有研究透彻,虽然在写完注释后对思路已经挺清楚了,但是细节上还是很欠缺。

这一篇博客是为动手撸一个服务器而准备的,在这篇博客里,会动手写一些基本的模块,并进行正确性验证。

变量命名没有很规范,因为有时想随意一点,有时又觉得想认真点,(●’◡’●)

环境:ubuntu20.04,c++11及以上(14更好,c++11的话编译器会对lambda表达式按值捕获发出警告,尽管代码还是能正确运行)

线程池

线程池是最简单的,就先从这入手。

考虑整体的思路:

  • 首先有一个池初始化若干个线程,每个线程配备一个运行的work函数,这个work函数要循环取任务去执行。
  • 取任务就有个任务队列,在设计任务队列前,我们想一下任务的形式是什么,要传什么样的什么类型的任务进来。
    • 一种选择是传入一个类对象,然后调用类对象的一些工作处理函数,并且还能根据类对象的一些参数来进行不同的处理。
    • 上面的方式也许是传统的c98的模式,它并不是那么的具有鲁棒性,因为尽管可以通过模板传入不同的类对象,但是类对象必须拥有一个处理函数。如果只是运行这些处理函数,为什么不直接传入一个函数进来呢,这样就可以执行任意的工作了。
    • c++11开始,std::function<>就提供了我们想要的功能,无论是函数指针还是函数对象等,都可以使用一个function对象保存然后传入。而那些细节处理交由上层,函数执行所需要的参数由std::bind来绑定给function对象,function本身不需要额外参数,更有灵活性。
  • 因此,我们的任务队列的元素就是std::function。然后,在取队列元素时和加入队列元素时,都需要互斥。队列是一个无界生产者,当队列大小为0时,一般用一个信号量阻塞即可,但我们也知道可以用条件变量来完成这件事,因此这里会用条件变量。
    • 在取任务时,首先互斥锁住(判断是否非空前就要锁),然后如果队列非空就取一个任务,解锁,执行任务。如果没有任务,就阻塞,这里用条件变量阻塞。因为要锁和解锁以及给条件变量使用,所以用unique_lock好,但unique_lock不要写在while内,这样会重复定义。
  • 这里先不考虑左值右值,因为本身项目中就不需要用到,从理论上讲考虑的话就更灵活,但没有实例还是不好理解,就先不用了。

现在,我们可以开始写代码了,这里提一下头文件(注意标准库一般都是std命名空间):

1
2
3
4
5
<mutex>:互斥锁等
<thread>:线程
<condition_variable>:条件变量
<functional>:std::function
<queue>:队列
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
//threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include<mutex>
#include<thread>
#include<condition_variable>
#include<functional>
#include<queue>

#include <cassert>//使用assert函数
class threadpool
{
private:
std::mutex mtx;//互斥锁
std::queue<std::function<void()>> taskQueue;//任务队列,无参数的function,调用时不用传参
std::condition_variable cond;//条件变量
public:
threadpool(int threadnum = 8)
{
assert(threadnum > 0);//没有线程就报错
for(int i=0;i<threadnum;i++)//创建线程池
std::thread([&]{//lambda表达式,按引用捕获变量(确保同一地址),无参数无返回值
std::unique_lock<std::mutex> locker(mtx);//定义一个locker对象,现在已经锁住了
while(true)
{
if(!taskQueue.empty())//如果有任务
{
auto task = taskQueue.front();
taskQueue.pop();
locker.unlock();
//解锁后再执行
task();
//执行完了,进入下一轮循环,注意要锁住
locker.lock();
}
else//如果没有任务
cond.wait(locker);//解锁并等待,唤醒后会抢占互斥锁
}
}).detach();//把thread分离,不用手动join,结束自动回收
}

void addTask(std::function<void()> task)
{
std::lock_guard<std::mutex> locker(mtx);//定义一个locker对象
taskQueue.emplace(task);//这种方式,使用emplace和push没啥区别,task本身就是临时对象
//如果要真正使用到emplace调用构造函数,还要配合std::forward完美转发,此时无论构造函数是不是explicit(不能隐式转换),都可以正常工作
cond.notify_one();//插入一个元素唤醒一个线程
}

~threadpool()//析构函数
{

}
};
#endif

上面的版本基本上已经写好了一个线程池,但还有析构函数没有写。析构函数要析构什么呢,这里好像并没有要手动释放的东西——没有手动使用堆空间,不需要delete。但别忘了,我们的线程是分离的,这里没有去join回收,线程使用的全局变量会产生所谓的detch陷阱。

当进程要退出时,不会再添加任务了,我们更希望线程把这些已有任务都做完再退出,但是主进程退出了会把队列、互斥锁、条件变量都析构掉,使得线程调用这些资源会出错,这就是detch()带来的陷阱。在讨论解决方案前,我们再想想为什么要使用detch():其实就是为了把已有的任务都做完。

为了做完这些任务,我们就得把线程访问的资源放在堆空间上,这样才不会在进程退出时析构掉资源。因为每个线程都访问这三个资源,因此再用一个结构体封装,线程捕获结构体指针就可以了。为了控制堆上的资源,我们还可以使用共享指针(共享很显然,因为这些线程用的是同一个)。

  • 创建共享指针的方式有两种,一种是用new,一种是用make_shared函数。尽量使用make_shared,因为new本质上会分配两次内存,一个是new的对象,一个是shared_ptr本身new的计数器(控制块)。而使用make_shared申请一个单独的内存块来存放对象和控制块,更高效,且因为没有顺序是同一时间开辟空间的,具有异常安全。C++11 make_shared - 简书 (jianshu.com)
  • 使用make_shared函数要显式指出类型,因为返回值是shared_ptr<T>,返回值类型无法推导,要make_shared<T>指出。
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
//改良版threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include<mutex>
#include<thread>
#include<condition_variable>
#include<functional>
#include<queue>

#include <cassert>//使用assert函数
class threadpool
{
private:
struct pool//封装三个资源
{
std::mutex mtx;//互斥锁
std::queue<std::function<void()>> taskQueue;//任务队列,无参数的function,调用时不用传参
std::condition_variable cond;//条件变量
};
std::shared_ptr<pool> pool_;//共享指针,pool_是一个指针指向pool结构体,这个指针用于线程池操作资源

public:
threadpool(int threadnum = 8):pool_(std::make_shared<pool>())//以make_shared的方式new一个对象给pool_指针
{
assert(threadnum > 0);//没有线程就报错
for(int i=0;i<threadnum;i++)//创建线程池
std::thread([pool_t = pool_]{//现在要按值捕获,相当于拷贝构造共享指针,计数+1,且指向相同内容
std::unique_lock<std::mutex> locker(pool_t->mtx);//定义一个locker对象,现在已经锁住了
while(true)
{
if(!pool_t->taskQueue.empty())//如果有任务
{
auto task = pool_t->taskQueue.front();
pool_t->taskQueue.pop();
locker.unlock();
//解锁后再执行
task();
//执行完了,进入下一轮循环,注意要锁住
locker.lock();
}
else//如果没有任务
pool_t->cond.wait(locker);//解锁并等待,唤醒后会抢占互斥锁
}
}).detach();//把thread分离,不用手动join,结束自动回收
}

void addTask(std::function<void()> task)
{
std::lock_guard<std::mutex> locker(pool_->mtx);//定义一个locker对象
pool_->taskQueue.emplace(task);//这种方式,使用emplace和push没啥区别,task本身就是临时对象
//如果要真正使用到emplace调用构造函数,还要配合std::forward完美转发,此时无论构造函数是不是explicit(不能隐式转换),都可以正常工作
pool_->cond.notify_one();//插入一个元素唤醒一个线程
}

~threadpool()//析构函数
{

}
};
#endif

现在,我们已经解决了detach陷进,只要线程执行完任务,我们就退出线程,那么我们的析构函数只需要传递一个信号,让线程得知主进程已经退出了,线程根据信号作出反应即可。注意,因为线程要把工作做完再退出,所以优先级是!empty(),如果没有任务,我们是先看要不要退出,再进行等待,所以在if-else中插入一个else if判断即可。

注意,线程有可能卡在条件变量的wait处,所以析构函数还要唤醒所有的线程,让它们处理任务(如果有)并退出。

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
//最终版threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include<mutex>
#include<thread>
#include<condition_variable>
#include<functional>
#include<queue>

#include <cassert>//使用assert函数
class threadpool
{
private:
struct pool//封装三个资源
{
std::mutex mtx;//互斥锁
std::queue<std::function<void()>> taskQueue;//任务队列,无参数的function,调用时不用传参
std::condition_variable cond;//条件变量
bool isclose = false;//默认值是false
};
std::shared_ptr<pool> pool_;//共享指针,pool_是一个指针指向pool结构体,这个指针用于线程池操作资源

public:
threadpool(int threadnum = 8):pool_(std::make_shared<pool>())//以make_shared的方式new一个对象给pool_指针
{
assert(threadnum > 0);//没有线程就报错
for(int i=0;i<threadnum;i++)//创建线程池
std::thread([pool_t = pool_]{//现在要按值捕获,相当于拷贝构造共享指针,计数+1,且指向相同内容
std::unique_lock<std::mutex> locker(pool_t->mtx);//定义一个locker对象,现在已经锁住了
while(true)
{
if(!pool_t->taskQueue.empty())//如果有任务
{
auto task = pool_t->taskQueue.front();
pool_t->taskQueue.pop();
locker.unlock();
//解锁后再执行
task();
//执行完了,进入下一轮循环,注意要锁住
locker.lock();//抢占锁
}
else if(pool_t->isclose)
break;
else//如果没有任务
pool_t->cond.wait(locker);//解锁并等待,唤醒后会抢占互斥锁
}
}).detach();//把thread分离,不用手动join,结束自动回收
}

void addTask(std::function<void()> task)
{
std::lock_guard<std::mutex> locker(pool_->mtx);//定义一个locker对象
pool_->taskQueue.emplace(task);//这种方式,使用emplace和push没啥区别,task本身就是临时对象
//如果要真正使用到emplace调用构造函数,还要配合std::forward完美转发,此时无论构造函数是不是explicit(不能隐式转换),都可以正常工作
pool_->cond.notify_one();//插入一个元素唤醒一个线程
}

~threadpool()//析构函数
{
pool_->isclose = true;
pool_->cond.notify_all();
}
};
#endif

test

以上基本就大功告成了,现在,我们来写一个test文件。

为了确认线程是否正确退出,我们在else if那打印退出信息:

1
2
3
4
else if(pool_t->isclose)
{
std::cout<<"thread exit!"<<std::endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//test_threadpool.cpp
#include "threadpool.h"
#include <unistd.h>//使用sleep
#include <iostraem>//cout
#include <pthread.h>//pthread_exit
using namespace std;
void task(int id)
{
cout<<"this is ["<<id<<"] task"<<endl;
sleep(id);//睡眠id秒
cout<<"["<<id<<"] task quit!"<<endl;
}
int main()
{
threadpool threadp(10);//十个线程
for(int i=5;i<20;i++)
threadp.addTask(bind(task,i));//bind 绑定task函数,并赋参数i,返回一个function对象
pthread_exit(NULL);//告知系统不用回收进程所有资源,等待子线程退出
return 0;//不能直接return!会把所有线程都回收
}

注意std::thread内部调用了pthread,linux不一定把pthread作为默认库,所以编译时候要链接,编译的命令为:

1
g++ -std=c++14 -o test_threadpool test_threadpool.cpp -lpthread

再者,当主进程return后,即使子线程是detch的,也会被系统回收资源。在 《UNIX 网络编程》卷一 第 537 页,有这么一句话:

1
如果进程的main函数返回或者任何线程调用了 exit, 整个进程就终止,其中包括它的任何线程。

程序return,间接调用了exit()函数,因为一个线程调用exit函数,导致整个进程的退出。要想系统并不回收进程的所有资源,可以调用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
//运行结果,数一下线程有没有正常退出
sun2@ubuntu:~/Desktop/websever_test$ g++ -std=c++14 -o test_threadpool test_threadpool.cpp -lpthread
sun2@ubuntu:~/Desktop/websever_test$ ./test_threadpool
this is [5] task
this is [6] task
this is [7] task
this is [8] task
this is [9] task
this is [10] task
this is [11] task
this is [12] task
this is [13] task
this is [14] task
[5] task quit!
this is [15] task
[6] task quit!
this is [16] task
[7] task quit!
this is [17] task
[8] task quit!
this is [18] task
[9] task quit!
this is [19] task
[10] task quit!
thread exit! //1
[11] task quit!
thread exit! //2
[12] task quit!
thread exit! //3
[13] task quit!
thread exit! //4
[14] task quit!
thread exit! //5
[15] task quit!
thread exit! //6
[16] task quit!
thread exit! //7
[17] task quit!
thread exit! //8
[18] task quit!
thread exit! //9
[19] task quit!
thread exit! //10
//test成功

日志系统

这一块比较复杂,分解知识点,一点点细学。

时间类chrono

这个在日志系统用了一点,不过也可以用来输出时间,就在这里学习了。

Duration

1
2
3
4
//duration表示一段时间间隔,用来记录时间长度,可以表示几秒钟、几分钟或者几个小时的时间间隔,duration的原型是:
template<class Rep, class Period = std::ratio<1>> class duration;
//第一个模板参数Rep是一个数值类型,表示时钟个数;第二个模板参数是一个默认模板参数std::ratio,它的原型是:
template<std::intmax_t Num, std::intmax_t Denom = 1> class ratio;

ratio表示每个时钟周期的秒数,其中第一个模板参数Num代表分子,Denom代表分母,分母默认为1,ratio代表的是一个分子除以分母的分数值,比如ratio<2>代表一个时钟周期是两秒,ratio<60>代表了一分钟,ratio<60*60>代表一个小时,ratio<60*60*24>代表一天。而ratio<1, 1000>代表的则是1/1000秒即一毫秒,ratio<1, 1000000>代表一微秒,ratio<1, 1000000000>代表一纳秒。标准库为了方便使用,就定义了一些常用的时间间隔,如时、分、秒、毫秒、微秒和纳秒,在chrono命名空间下,它们的定义如下:

1
2
3
4
5
6
7
8
9
10
typedef duration <Rep, ratio<3600,1>> hours;
typedef duration <Rep, ratio<60,1>> minutes;
typedef duration <Rep, ratio<1,1>> seconds;
typedef duration <Rep, ratio<1,1000>> milliseconds;
typedef duration <Rep, ratio<1,1000000>> microseconds;
typedef duration <Rep, ratio<1,1000000000>> nanoseconds;

//通过定义这些常用的时间间隔类型,我们能方便的使用它们,比如线程的休眠:
std::this_thread::sleep_for(std::chrono::seconds(3)); //休眠三秒
std::this_thread::sleep_for(std::chrono:: milliseconds (100)); //休眠100毫秒

chrono还提供了获取时间间隔的时钟周期个数的方法count() ,count()返回的间隔要向0取整,可以为负数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//chrono还提供了获取时间间隔的时钟周期个数的方法count(),它的基本用法:
#include <chrono>
#include <iostream>
int main()
{
std::chrono::milliseconds ms{3}; // 3 毫秒
// 6000 microseconds constructed from 3 milliseconds
std::chrono::microseconds us = 2*ms; //6000微秒
// 30Hz clock using fractional ticks
std::chrono::duration<double, std::ratio<1, 30>> hz30(3.5);
std::cout << "3 ms duration has " << ms.count() << " ticks\n"<< "6000 us duration has " << us.count() << " ticks\n"
}

输出:
3 ms duration has 3 ticks
6000 us duration has 6000 ticks

时间间隔之间可以做运算,比如下面的例子中计算两端时间间隔的差值:

1
2
3
4
5
//都是duration类型
std::chrono::minutes t1( 10 );
std::chrono::seconds t2( 60 );
std::chrono::seconds t3 = t1 - t2;
std::cout << t3.count() << " second" << std::endl;

其中,t1 是代表 10 分钟、t2 是代表 60 秒,t3 则是 t1 減去 t2,也就是 600 - 60 = 540 秒。通过t1-t2的count输出差值为540个时钟周期即540秒(因为每个时钟周期为一秒)。

还可以通过**duration_cast<>()**来将当前的时钟周期转换为其它的时钟周期,比如我可以把秒的时钟周期转换为分钟的时钟周期,然后通过count来获取转换后的分钟时间间隔:

1
2
3
cout << chrono::duration_cast<chrono::minutes>( t3 ).count() <<” minutes”<< endl;
将会输出:
9 minutes

使用转型后,数值就不一定是整数个tick()了,比如t2=30s时,转型后就是9分半,这时使用count()会向下取整,只取到9。

clock

谈到时间,总需要找一个时钟作为参照。clock就是这个时钟,在计算机中一般都会有一套或多套时钟系统供程序使用。在std::chrono库中,有3种时钟:

  • system_clock
  • steady_clock
  • hight_definition_clock
1
2
3
4
5
6
7
8
struct system_clock
{ // wraps GetSystemTimePreciseAsFileTime/GetSystemTimeAsFileTime
typedef long long rep;
typedef ratio_multiply<ratio<_XTIME_NSECS_PER_TICK, 1>, nano> period;
typedef chrono::duration<rep, period> duration;
typedef chrono::time_point<system_clock> time_point;
static constexpr bool is_steady = false;//steady_clock是true
//函数定义,注意都是静态成员函数,使用时用system_clock::func()调用

一般情况下,他们3个没有太大的区别,hight_definition_clock、steady_clock仅仅是system_clock的typedef,但是有为什么要区分呢,因为在有些情况下,他们是存在差异的。

  • 情况1:system_clock和steady_clock的差异
    • 比如windows系统可以提供时钟,如果认为时间不准,我们还可以进行调整。在没有调整时间前,system_clock和steady_clck是一样的,他们的读数都是单调匀速增加的;但是如果调整时间后,它们两者的读数就会出现差异,system_clock的读数就会出现跳变,而steady_clock依然保持线性单调递增,不受clock调整的影响,这个特点非常方便我们统计时间耗时(duration)。
  • 情况2:system_clock与hight_definition_clock的差异
    • 如果系统提供的时钟(clock)不止一种,有的时钟精度高(分辨率),有的精度低,hight_definition_clock使用时精度最高的clock,但是system_clock就不一定了。

clock主要用于获取当前的时间,通过now()方法获取,返回一个time_point,方法如下:

1
std::chrono::system_clock::time_point current_time = std::chrono::system_clock::now();

还有两个函数方法:to_time_t():参数是time_point,转换到time_t;from_time_t():从time_t转换过来。

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
// system_clock example
#include <chrono>
#include <iostream>
int main()
{
std::chrono::duration<int, std::ratio<60*60*24> > one_day(1);

// 根据时钟得到现在时间
std::chrono::system_clock::time_point today = std::chrono::system_clock::now();
std::time_t time_t_today = std::chrono::system_clock::to_time_t(today);
std::cout << "now time stamp is " << time_t_today << std::endl;
std::cout << "now time is " << ctime(&time_t_today) << std::endl;


// 看看明天的时间,time_point支持一些算术元算,比如两个time_point的差值时钟周期数,还可以和duration相加减
std::chrono::system_clock::time_point tomorrow = today + one_day;
std::time_t time_t_tomorrow = std::chrono::system_clock::to_time_t(tomorrow);
std::cout << "tomorrow time stamp is " << time_t_tomorrow << std::endl;
std::cout << "tomorrow time is " << ctime(&time_t_tomorrow) << std::endl;


// 计算下个小时时间
std::chrono::system_clock::time_point next_hour = today + std::chrono::hours(1);
std::time_t time_t_next_hour = std::chrono::system_clock::to_time_t(next_hour);
std::chrono::system_clock::time_point next_hour2 = std::chrono::system_clock::from_time_t(time_t_next_hour);

std::time_t time_t_next_hour2 = std::chrono::system_clock::to_time_t(next_hour2);
std::cout << "tomorrow time stamp is " << time_t_next_hour2 << std::endl;
std::cout << "tomorrow time is " << ctime(&time_t_next_hour2) << std::endl;

return 0;
}
//
now time stamp is 1586662332
now time is Sun Apr 12 11:32:12 2020

tomorrow time stamp is 1586748732
tomorrow time is Mon Apr 13 11:32:12 2020

tomorrow time stamp is 1586665932
tomorrow time is Sun Apr 12 12:32:12 2020

time_point

time_point是具体的时间,比如某年某月某日几点几分几秒,time_point依赖于clock的计时,可以用clock内部定义的time_point,也可以用自己定义的time_point。

1
2
//一个模板是时钟类型clock,一个是计时间隔duration
template <class Clock, class Duration = typename Clock::duration> class time_point;

time_point有一个函数time_from_epoch()用来获得1970年1月1日到time_point时间经过的duration。举个例子,如果timepoint以天为单位,函数返回的duration就以天为单位。

由于各种time_point表示方式不同,chrono也提供了相应的转换函数 time_point_cast。

1
2
template <class ToDuration, class Clock, class Duration>
time_point<Clock,ToDuration> time_point_cast (const time_point<Clock,Duration>& tp);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//计算当前时间距离1970年1月一日有多少天:
#include <iostream>
#include <ratio>
#include <chrono>

int main ()
{
using namespace std::chrono;
typedef duration<int,std::ratio<60*60*24>> days_type;
//now获取后返回的是system_clock类的timepoint类型,转型为天,给用户定义的timepoint
time_point<system_clock,days_type> today = time_point_cast<days_type>(system_clock::now());
//调用epoch获得duration,调用count()计数
std::cout << today.time_since_epoch().count() << " days since epoch" << std::endl;
return 0;
}

小结

这部分主要是三种类型穿插,让人比较迷糊。一般的用法就是:

  • 使用一个时钟的time_point,通过now()方法获取
  • 要想直接输出就把获取的time_point通过to_time_t()转换后输出,这里还可以进一步用ctime函数格式化(返回char*)。注意time_point不能直接输出。
  • 如果要继续处理,有两种运算方式
    • 定义duration,与time_point进行运算(一般是求和),运算后又是一个time_point,time_point之间本身可以大小比较
    • time_point之间作差,返回一个duration,可以用duration_cast转换类型,调用count()计算有多少tick。如果不转类型,system_clock的time_point一般是纳秒,用count的话很大。

test

主要实现几个实例吧:

  • 实现时间格式化输出:
    • 当前时间
    • 两小时后
    • 两天后
  • 实现时间点大小比较
  • 实现时间点运算count。
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
//时间类demo
#include<iostream>
#include<chrono>

using namespace std;
void printft(chrono::system_clock::time_point time)
{
time_t tt = chrono::system_clock::to_time_t(time);
//timepoint不能直接输出
//cout << "timepoint"<< time <<endl << "time:" << tt <<endl << "ctime:"<<ctime(&tt)<<endl<<endl;
cout <<"time:" << tt <<endl << "ctime:"<<ctime(&tt)<<endl<<endl;
}
int main()
{
//now
chrono::system_clock::time_point nowtime = chrono::system_clock::now();
cout<<"--------------now time----------------------"<<endl;
printft(nowtime);

//定义duration
chrono::hours twoh(2);
chrono::time_point<chrono::system_clock> twohtime = nowtime+twoh;
cout<<"--------------two hours after----------------------"<<endl;
printft(twohtime);

//自定义一天的间隔,一天的秒数是60*60*24
typedef chrono::duration<int, std::ratio<60*60*24> > day;
day twod(2);
chrono::time_point<chrono::system_clock> twodtime = nowtime+twod;
cout<<"--------------two days after----------------------"<<endl;
printft(twodtime);

cout<<"--------------time_point之间比较大小----------------------"<<endl;
const char *str = (twodtime>nowtime)?"大" : "小";
cout<<"两天后时间比两天前要"<<str<<endl;

cout<<"--------------timepoint之间作差转型查看时间点间隔tick----------------------"<<endl;
cout<<"两天后和两天前间隔 "<<chrono::duration_cast<chrono::hours>(twodtime-nowtime).count()<<" 小时"<<endl;
cout<<"system_clock time_point不转换类型tick:"<<(twodtime-nowtime).count()<<endl;
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
//编译命令
g++ -std=c++14 -o chrono_test chrono_test.cpp
//输出
sun2@ubuntu:~/Desktop/websever_test/chrono_test$ ./chrono_test
--------------now time----------------------
time:1664526100
ctime:Fri Sep 30 01:21:40 2022


--------------two hours after----------------------
time:1664533300
ctime:Fri Sep 30 03:21:40 2022


--------------two days after----------------------
time:1664698900
ctime:Sun Oct 2 01:21:40 2022


--------------time_point之间比较大小----------------------
两天后时间比两天前要大
--------------timepoint之间作差转型查看时间点间隔tick----------------------
两天后和两天前间隔 48 小时
system_clock time_point不转换类型tick:172800000000000//这里可以看出是纳秒

可变参宏va_list

变长参数

在无法给出所有传递给函数的参数的类型和数目时,可以使用省略号(…)指定函数参数表。有如下几种形式:

1
2
3
void fun1(int a, double b, ...); //给出确定的几个参数,其他用省略号
void fun2(int a ...); //省略号前有或者没有逗号都是可以的
void fun3(...); //也可以不确定任何参数,但和没有参数是不一样的

最典型的应用就是printf函数,printf的声明和调用方法如下:

1
2
int printf( const char *format [,argument]... );    //官方声明
printf("My name is %s, age %d.", "AnnieKim", 24); //调用

通常情况下,第一个参数是必不可少的,因为它可以得到函数参数的地址入口,这样就可以取之后的参数。

1
2
3
4
5
6
7
8
9
10
11
12
可变参数是由宏实现的,但是由于硬件平台的不同,编译器的不同,宏的定义也不相同
头文件<stdarg.h>
typedef char * va_list; // TC中定义为void*
//为了满足需要内存对齐的系统
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )

//ap指向第一个变参的位置,即将第一个变参的地址赋予ap
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
/*获取变参的具体内容,t为变参的类型,如有多个参数,则通过移动ap的指针来获得变参的地址,从而获得内容*/
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
//清空va_list,即结束变参的获取
#define va_end(ap) ( ap = (va_list)0 )

基本使用步骤:

  • 定义一个va_list类型的变量,变量是指向参数的指针。
  • va_start初始化刚定义的变量,第二个参数是最后一个显式声明的参数。
  • va_arg返回变长参数的值,第二个参数是该变长参数的类型
  • va_end将第一步定义的va_list变量重置为NULL。

注意问题:

(1)可变参数的类型和个数完全由程序代码控制,它并不能智能地识别不同参数的个数和类型;

(2)如果我们不需要一一详解每个参数,只需要将可变列表拷贝至某个缓冲,可用vsprintf函数;

(3)因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码;

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
//C 库函数 int vsprintf(char *str, const char *format, va_list arg) 使用参数列表发送格式化输出到字符串。
int vsprintf(char *str, const char *format, va_list arg)
//即把参数列表中遍历到的一个一个参数,根据format格式写到str里。

//例子
#include <stdio.h>
#include <stdarg.h>

char buffer[80];
int vspfunc(char *format, ...)
{
va_list aptr;
int ret;

va_start(aptr, format);
ret = vsprintf(buffer, format, aptr);
va_end(aptr);

return(ret);
}

int main()
{
int i = 5;
float f = 27.0;
char str[50] = "runoob.com";

vspfunc("%d %f %s", i, f, str);
printf("%s\n", buffer);

return(0);
}
//结果
5 27.000000 runoob.com

还有vsnprintf,多了个size,size说明了str最多可写的字节,防止越界

1
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

test

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<iostream>
#include<stdarg.h>
#include<string>

//测试va_arg,va_arg不会自动结束,不会有=""返回
/*错误代码,va_arg读完还读下去会内存错误
void valist(const char* str1,...)
{
va_list vaList;
va_start(vaList,str1);
std::string str;
while((str = va_arg(vaList,const char*))!="")
std::cout<<str<<std::endl;
va_end(vaList);
}
*/

//遵循古老的传统,要么加个结束元判断结束,要么让第一个参数指明参数个数手动结束
//加结束元
void valist1(const char* str1,...)
{
va_list vaList;
va_start(vaList,str1);
std::string str = str1;//va_arg从str1的下一个参数开始,str1这个参数自己获取
while(str!="break")
{
std::cout<<str<<std::endl;
str = va_arg(vaList, const char*);//获取下一个参数
}
//结束
va_end(vaList);
}

void valist2(int arglen ,const char* str1,...)
{
va_list vaList;
va_start(vaList,str1);
std::string str = str1;//va_arg从str1的下一个参数开始,str1这个参数自己获取
for(int i=1;i<arglen;i++)//i=1开始是因为第一个已经获取
{
std::cout<<str<<std::endl;
str = va_arg(vaList, const char*);//获取下一个参数
}
std::cout<<str<<std::endl;//这里要多打印一次,因为最后一次取到参数没打印就退出了
//结束
va_end(vaList);
}


int main()
{
std::cout<<std::endl;
valist1("hello","myfriend","nihaoya","break");

std::cout<<std::endl;
valist2(5,"hi","wish you","happy","health","every day");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//运行情况
sun2@ubuntu:~/Desktop/websever_test/valist$ g++ -std=c++14 -o valist_test valist_test.cpp
sun2@ubuntu:~/Desktop/websever_test/valist$ ./valist_test

hello
myfriend
nihaoya

hi
wish you
happy
health
every day
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
//vsprintf使用
#include<iostream>
#include<stdarg.h>

void vsptest(char* buffer,const char* format,...)
{
va_list vaList;
va_start(vaList,format);
//解析format这个格式,把后面的参数按照格式填入,格式中暗示了参数的类型
vsprintf(buffer,format,vaList);
va_end(vaList);
}


void vsnptest(char* buffer,size_t size,const char* format,...)
{
va_list vaList;
va_start(vaList,format);
//解析format这个格式,把后面的参数按照格式填入,格式中暗示了参数的类型
vsnprintf(buffer,size,format,vaList);//测试一下这个size,看看会怎么样
va_end(vaList);
}

int main()
{
std::cout<<"----------测试vsprintf-------------"<<std::endl;
char buffer1[50];
const char *format1 = "%s is %d years old, %s";
vsptest(buffer1,format1,"jy",20,"good!");
std::cout<<buffer1<<std::endl<<std::endl;

std::cout<<"----------测试vsnprintf-------------"<<std::endl;
char buffer2[50];
const char *format2 = "%s is %d years old, %s";
vsnptest(buffer2,10,format2,"xuepi",20,"nice!");//只有10的size
std::cout<<"只有10的buffer size: "<<buffer2<<std::endl;
vsnptest(buffer2,50,format2,"xuepi",20,"nice!");//50
std::cout<<"拥有50的buffer size: "<<buffer2<<std::endl<<std::endl;

//如果size超出了buffer的大小会怎么样呢
std::cout<<"----------测试vsnprintf,并且size超出了buffer的大小-------------"<<std::endl;
char buffer3[10];
vsnptest(buffer3,50,format2,"xuepi",20,"nice!");//有50的size
std::cout<<buffer3<<std::endl;

return 0;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//测试结果
sun2@ubuntu:~/Desktop/websever_test/valist$ g++ -std=c++14 -o vstest vstest.cpp
sun2@ubuntu:~/Desktop/websever_test/valist$ ./vstest
----------测试vsprintf-------------
jy is 20 years old, good!

----------测试vsnprintf-------------
只有10的buffer size: xuepi is
拥有50的buffer size: xuepi is 20 years old, nice!

----------测试vsnprintf,并且size超出了buffer的大小-------------
xuepi is 20 years old, nice!
//可以看出即使超出了buffer的size也不会报错,而是继续向buffer拷贝,打印时因为首地址的关系会全打印出来
//这个size参数是给vsnprintf的,告诉它最多写多少进buffer,可以与buffer本身的大小无关,但一般会关联到buffer的大小

小结

四部曲,其中va_arg和vsprintf互相替换,看要哪个。va_arg就只能传一样的参数类型,可以做运算,vsprintf可以传不同的类型但是最后要写入一个char*的buffer。

也可以两个同时用,取完va_arg的参数,剩下的给vsprintf。

vsprintf和vsnprintf返回写入的字节数。

阻塞队列

阻塞队列本质上是在队列的基础上封装,添加阻塞的功能。主要就是一个普通的queue,然后用互斥锁和条件变量保护。条件变量是因为这个阻塞队列可以看成一个缓冲区,然后要生产者和消费者,因此条件变量替代信号量管理缓冲区。

当缓冲区已满时,生产者需要等待,由于是多个生产者竞争,所以要使用while-wait的等待方式。一旦push任务成功,就唤醒一个消费者线程。

当缓冲区已空时,消费者需要等待,和前面的方式一样。消费者还要支持超时处理,等待时间太长就不等待。

基本的操作都很简单,麻烦的地方在于关闭时的处理,要让在阻塞的线程退出,且不允许再操作,调用push和pop直接返回。

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
//阻塞队列version1
#ifndef BLOCKQUEUE_H
#define BLOCKQUEUE_H

#include <mutex>
#include <queue>
#include <condition_variable>
#include <cassert>
#include <chrono>
template<class T>
class blockqueue
{
private:
std::queue<T> que;
std::mutex mux;
std::condition_variable condprod;
std::condition_variable condcons;
size_t size;
public:
blockqueue(int maxsize = 1024):size(maxsize)
{
assert(maxsize>0);//初始化检查
}

~blockqueue()
{
close();
}

void close();
void clear();//queue没有clear操作,自己支持

bool empty();
bool full();

void push(const T &task);
T pop();
T pop(int timeout);


};


template<class T>
bool blockqueue<T>::empty()
{
std::lock_guard<std::mutex> locker(mux);
return que.empty();
}

template<class T>
bool blockqueue<T>::full()
{
std::lock_guard<std::mutex> locker(mux);
return que.size()>=size;
}

template<class T>
void blockqueue<T>::push(const T &task)
{
//插入元素,首先抢占互斥锁,但即使抢占了互斥锁也可能不能插入,队列可能是满的,这时要释放锁让消费者线程获得锁
std::unique_lock<std::mutex> locker(mux);//要用条件变量,用unique锁
while(que.size()>=size)//避免虚假唤醒,notify_one一般不会导致虚假唤醒,但要随时最好准备。并且当要关闭时会notify_all
condprod.wait(locker);//等待唤醒
que.push(task);//插入元素
condcons.notify_one();//唤醒消费者

}

template<class T>
T blockqueue<T>::pop()
{
std::unique_lock<std::mutex> locker(mux);
while(que.empty())
condcons.wait(locker);
T task = que.front();
que.pop();
condprod.notify_one();
return task;
}

template<class T>
T blockqueue<T>::pop(int timeout)
{
std::unique_lock<std::mutex> locker(mux);
while(que.empty())//为空就等待,等待过程中如果超时就返回
if(condcons.wait_for(locker,std::chrono::seconds(timeout)) == std::cv_status::timeout)//超时
return T(0);

T task = que.front();
que.pop();
condprod.notify_one();
return task;
}

template<class T>
void blockqueue<T>::close()
{
/*
* 当关闭时,我们的目标是要让所有的线程都退出,也就是不能被阻塞到push和pop里
* 调用此函数的时机是,上层日志系统已经把任务都做完,然后关闭队列
* 则调用这个函数后,所有想再次尝试push和pop的线程都不允许
*/
std::lock_guard<std::mutex> locker(mux);//要锁住,然后clear队列
clear();
//做些事通知push和pop都不允许了
}
template<class T>
void blockqueue<T>::clear()
{
//close已经锁住了,不用锁了
//高效的方式,swap一个空队列
std::queue<T> empty;
std::swap(empty, que);
}

#endif

上面是一个较为完整的阻塞队列了,也是一般思考的形式,但还是有些没有完成的地方和不足。

首先我们思考一下,上层取任务的形式应该是循环pop,那么这个pop函数就需要返回一个成功或失败(在close后)的信息才能让上层线程结束循环,并且由于增加了超时处理,等待失败我们想什么都不返回,这和现在的代码不太相同。因此更好的方式是返回bool值,而取出的任务以引用参数形式传出。

1
2
3
4
5
6
7
8
9
10
11
12
13
template<class T>
bool blockqueue<T>::pop(T& task, int timeout)
{
std::unique_lock<std::mutex> locker(mux);
while(que.empty())//为空就等待,等待过程中如果超时就返回
if(condcons.wait_for(locker,std::chrono::seconds(timeout)) == std::cv_status::timeout)//超时
return false;

task = que.front();
que.pop();
condprod.notify_one();
return true;
}

然后,我们思考一下怎么通知pop和push在close后直接退出,通常的方式是使用一个close信号,像线程池那样。最简单的方式就是在pop和push入口处设置判断,如果close就直接返回false(push直接return,不做任何事情)。然而:

  • 当上层一直调用完pop后,线程会卡在wait处,或者正在准备调用再次pop。
  • 如果有生产者一直没抢夺到互斥锁,而被消费者占用,那么在上层pop结束后,很多的push可能会导致队列又满,从而push线程阻塞在wait处。

我们先处理pop函数,我们必须唤醒所有在等待的消费者线程,然后让线程得知已close退出;对于没有在等待而是准备进入的线程,因为队列是空,不能让其进入while,否则会阻塞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T>
bool blockqueue<T>::pop(T& task, int timeout)
{
std::unique_lock<std::mutex> locker(mux);
while(que.empty() and !isclose)//为空就等待,等待过程中如果超时就返回。如果是关闭状态就不进入
if(condcons.wait_for(locker,std::chrono::seconds(timeout)) == std::cv_status::timeout)//超时
return false;

if(isclose)//关闭的信号
return false;
task = que.front();
que.pop();
condprod.notify_one();
return true;
}

现在我们处理push函数

  • 在关闭前,所有任务会被执行完然后消费者阻塞,这个操作由上层循环查看队列是否为空来做;注意这是日志系统析构时的操作,而消费者线程的循环是看pop的返回值,这样可以一直阻塞取任务直到超时或者close。在判断是否为空这个过程,既可以push也可以pop。
  • 为空后在调用close前,有一段真空期可以push,同时也可以pop,这可能导致有些生产者阻塞了;
  • 然后上层在执行完任务后调用close,close去抢占互斥锁,清空队列(那些真空期push的),设置信号,唤醒所有的消费者线程让它们退出。
  • 由于队列清空了,此时while会被break,则唤醒阻塞的生产者并让其退出,且退出不写在while里,因为后来的生产者不会进入while,写外面让它们直接退出。这样维持队列是空,接下来的消费者线程因为进入while也退出了。
1
2
3
4
5
6
7
8
9
10
11
12
template<class T>
void blockqueue<T>::push(const T &task)
{
//插入元素,首先抢占互斥锁,但即使抢占了互斥锁也可能不能插入,队列可能是满的,这时要释放锁让消费者线程获得锁
std::unique_lock<std::mutex> locker(mux);//要用条件变量,用unique锁
while(que.size()>=size)//避免虚假唤醒,notify_one一般不会导致虚假唤醒,但要随时最好准备。并且当要关闭时会notify_all
condprod.wait(locker);//等待唤醒
if(isclose)
return;//不能插入元素
que.push(task);//插入元素
condcons.notify_one();//唤醒消费者
}

现在close函数就出来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class T>
void blockqueue<T>::close()
{
/*
* 当关闭时,我们的目标是要让所有的线程都退出,也就是不能被阻塞到push和pop里
* 调用此函数的时机是,上层日志系统已经把任务都做完,然后关闭队列
* 则调用这个函数后,所有想再次尝试push和pop的线程都不允许
*/
std::lock_guard<std::mutex> locker(mux);//要锁住,然后clear队列
que.clear();
isclose = true;
condprod.notify_all();
condcons.notify_all();
}

现在是最终版

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
#ifndef BLOCKQUEUE_H
#define BLOCKQUEUE_H

#include <mutex>
#include <queue>
#include <condition_variable>
#include <cassert>
#include <chrono>
template<class T>
class blockqueue
{
private:
std::queue<T> que;
std::mutex mux;
std::condition_variable condprod;
std::condition_variable condcons;
size_t size;
bool isclose;
public:
blockqueue(int maxsize = 1024):size(maxsize),isclose(false)
{
assert(maxsize>0);//初始化检查
}

~blockqueue()
{
close();
}

void close();
void clear();
bool empty();
bool full();

void push(const T &task);
bool pop(T& task);
bool pop(T& task, int timeout);


};


template<class T>
bool blockqueue<T>::empty()
{
//这是日志系统调用的函数,阻塞队列的pop不能调用,否则会死锁
std::lock_guard<std::mutex> locker(mux);
return que.empty();
}

template<class T>
bool blockqueue<T>::full()
{
//这是日志系统调用的函数,阻塞队列的push不能调用,否则会死锁
std::lock_guard<std::mutex> locker(mux);
return que.size()>=size;
}

template<class T>
void blockqueue<T>::push(const T &task)
{
//插入元素,首先抢占互斥锁,但即使抢占了互斥锁也可能不能插入,队列可能是满的,这时要释放锁让消费者线程获得锁
std::unique_lock<std::mutex> locker(mux);//要用条件变量,用unique锁
while(que.size()>=size)//避免虚假唤醒,notify_one一般不会导致虚假唤醒,但要随时最好准备。并且当要关闭时会notify_all
condprod.wait(locker);//等待唤醒
if(isclose)
return;//不能插入元素
que.push(task);//插入元素
condcons.notify_one();//唤醒消费者
}

template<class T>
bool blockqueue<T>::pop(T& task)
{
std::unique_lock<std::mutex> locker(mux);
while(que.empty() and !isclose)
condcons.wait(locker);

if(isclose)//关闭的信号
return false;

task = que.front();
que.pop();
condprod.notify_one();
return true;
}

template<class T>
bool blockqueue<T>::pop(T& task, int timeout)
{
std::unique_lock<std::mutex> locker(mux);
while(que.empty() and !isclose)//为空就等待,等待过程中如果超时就返回
if(condcons.wait_for(locker,std::chrono::seconds(timeout)) == std::cv_status::timeout)//超时
return false;

if(isclose)//关闭的信号
return false;
task = que.front();
que.pop();
condprod.notify_one();
return true;
}

template<class T>
void blockqueue<T>::close()
{
/*
* 当关闭时,我们的目标是要让所有的线程都退出,也就是不能被阻塞到push和pop里
* 调用此函数的时机是,上层日志系统已经把任务都做完,然后关闭队列
* 则调用这个函数后,所有想再次尝试push和pop的线程都不允许
*/
std::lock_guard<std::mutex> locker(mux);//要锁住,然后clear队列
clear();
isclose = true;//修改信号
//唤醒所有线程
condprod.notify_all();
condcons.notify_all();
}

template<class T>
void blockqueue<T>::clear()
{
//close已经锁住了,不用锁了
//高效的方式,swap一个空队列
std::queue<T> empty;
std::swap(empty, que);
}

#endif

test

日志主要是写入字符串,这里就模拟字符串放入阻塞队列,然后取出打印

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
#include <iostream>
#include <thread>
#include "blockqueue.h"
#include <string>
#include <pthread.h>
#include <unistd.h>//使用sleep
using namespace std;

int main()
{
shared_ptr<blockqueue<string>> blockque(new blockqueue<string>(1024));//主线程和消费者线程共享


//初始化消费者线程
thread([blockque_ = blockque]{
string str;
while(blockque_->pop(str))
{
cout<<"thread1 pop: "<<str<<endl;
sleep(3);
}
}).detach();

thread([blockque_ = blockque]{
string str;
while(blockque_->pop(str))
{
cout<<"thread2 pop: "<<str<<endl;
sleep(3);
}
}).detach();

//初始化生产者线程
thread([blockque_ = blockque]{
for(int i=0;i<20;i++)
{
string str;
str = "theard number[" + to_string(i)+"]";
blockque_->push(str);
cout<<"push: "<<str<<endl;
sleep(1);
}
}).detach();

thread([blockque_ = blockque]{
for(int i=20;i<40;i++)
{
string str;
str = "theard number[" + to_string(i)+"]";
blockque_->push(str);
cout<<"push: "<<str<<endl;
sleep(1);
}
}).detach();

//等一下生产者
sleep(2);

//调用close前,要等线程做完
while(!blockque->empty());

blockque->close();

//测试close后还能不能push
thread([blockque_ = blockque]{
for(int i=20;i<40;i++)
{
string str;
str = "theard number[" + to_string(i)+"]";
blockque_->push(str);
}
}).detach();
string s = (blockque->empty())?"true":"false";
cout<<"close 之后 push,现在阻塞队列是否为空:"<< s <<endl;

pthread_exit(NULL);
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
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
sun2@ubuntu:~/Desktop/websever_test/blockqueue$ g++ -std=c++14 -o blockque_test  blockque_test.cpp -lpthread//编译
sun2@ubuntu:~/Desktop/websever_test/blockqueue$ ./blockque_test
push: theard number[0]
thread1 pop: theard number[0]
push: theard number[20]
thread2 pop: theard number[20]
push: theard number[1]
push: theard number[21]
push: theard number[22]
push: theard number[2]
thread1 pop: theard number[1]
thread2 pop: theard number[21]
push: theard number[23]
push: theard number[3]
push: theard number[24]
push: theard number[4]
push: theard number[25]
push: theard number[5]
thread1 pop: theard number[22]
thread2 pop: theard number[2]
push: theard number[26]
push: theard number[6]
push: theard number[27]
push: theard number[7]
push: theard number[28]
push: theard number[8]
thread1 pop: theard number[23]
thread2 pop: theard number[3]
push: theard number[29]
push: theard number[9]
push: theard number[30]
push: theard number[10]
push: theard number[31]
push: theard number[11]
thread1 pop: theard number[24]
thread2 pop: theard number[4]
push: theard number[32]
push: theard number[12]
push: theard number[33]
push: theard number[13]
push: theard number[34]
push: theard number[14]
thread1 pop: theard number[25]
thread2 pop: theard number[5]
push: theard number[35]
push: theard number[15]
push: theard number[36]
push: theard number[16]
push: theard number[37]
push: theard number[17]
thread2 pop: theard number[26]
thread1 pop: theard number[6]
push: theard number[38]
push: theard number[18]
push: theard number[39]
push: theard number[19]
thread1 pop: theard number[27]
thread2 pop: theard number[7]
thread1 pop: theard number[28]
thread2 pop: theard number[8]
thread1 pop: theard number[29]
thread2 pop: theard number[9]
thread1 pop: theard number[30]
thread2 pop: theard number[10]
thread1 pop: theard number[31]
thread2 pop: theard number[11]
thread1 pop: theard number[32]
thread2 pop: theard number[12]
thread1 pop: theard number[33]
thread2 pop: theard number[13]
thread1 pop: theard number[34]
thread2 pop: theard number[14]
thread1 pop: theard number[35]
thread2 pop: theard number[15]
thread1 pop: theard number[36]
thread2 pop: theard number[16]
thread1 pop: theard number[37]
thread2 pop: theard number[17]
thread1 pop: theard number[38]
thread2 pop: theard number[18]
thread1 pop: theard number[39]
thread2 pop: theard number[19]
close 之后 push,现在阻塞队列是否为空:true//这说明close之后,所有的生产者都放不进去

可以看到pop和push都顺利完成,且close之后进程能正常退出,且任务不会再push进去,效果还可以。

小结

我们再来梳理一下close部分:

  • 在调用close前,日志系统会使用循环判断empty(),必须要队列为空即任务做完才close。
  • 当empty的时候,有的消费者线程会阻塞,有的消费者线程可能还没结束执行,准备重新进入。而生产者依旧可以放入任务,并且消费者也可以执行任务。
  • 一旦close函数抢占到了互斥锁,接下来所有的push和pop都是禁止的:
    • 首先close会把队列清空,无论是原来已经执行完了,还是生产者在真空期放了些任务进去但没做的(关闭后放进来的不算)
    • 然后唤醒所有在等待的消费者,清空后队列大小是0:
      • 对于生产者,在真空期可能会大量放入任务导致阻塞,这里要唤醒;也有的后来想push的,因为队列大小是0不会进入while,直接根据close信号退出。
      • 对于消费者,把阻塞的线程唤醒退出,但注意有的消费者可能正准备从头进入,唤醒后由于队列是空不能在让其进入while阻塞,不然会死锁。因此while的判断要加入close信号,要退出就不进入等待,直接退出。
    • 这样阻塞队列就关闭了,遗留的任务会写完,在阻塞的线程会退出,想调用的线程也会直接退出。

write

write()函数是日志系统中最重要的函数,进行主要的业务处理。我们给日志系统几点要求:

  • 分级别,比如info、debug、warning、error,可以设定日志系统的等级,级别越低,能写入的越多。这要求write函数传入一个指示level的变量。
  • 像printf一样支持各种形式的信息,比如float、char*、int,这可以使用可变参宏来实现,只需要向write函数传入一个format,然后传入一系列参数即可。
  • 不把所有的日志都只写入一个文件:
    • 当换了一天时,关闭原来的文件,新建一个文件,这要求系统记录day信息;
    • 当一天的日志行数(一个文件行数)过多时,换一个文件;
    • 文件命名的一个实例为:2022-10-03_log0.txt;其中log0表示这是这一天的第一份文件
  • 一行日志信息的一个实例为:[info]2022-10-03_21:25:09:this is info

首先要处理一下时间的格式化,前面使用的ctime函数可以获得我们想要的信息,但并不是这里提到的格式化,并且我们还需要单独的day的信息。一个想法是使用一个结构体,解析ctime的返回值,把年月日时分秒存在结构体里。实际上这个结构体在c++中已经有了,是time.h中的tm结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef _TM_DEFINED
struct tm {
int tm_sec; /* 秒 – 取值区间为[0,59] */
int tm_min; /* 分 - 取值区间为[0,59] */
int tm_hour; /* 时 - 取值区间为[0,23] */
int tm_mday; /* 一个月中的日期 - 取值区间为[1,31] */
int tm_mon; /* 月份(从一月开始,0代表一月) - 取值区间为[0,11] */
int tm_year; /* 年份,其值等于实际年份减去1900 */
int tm_wday; /* 星期 – 取值区间为[0,6],其中0代表星期天,1代表星期一,以此类推 */
int tm_yday; /* 从每年的1月1日开始的天数 – 取值区间为[0,365],其中0代表1月1日,1代表1月2日,以此类推 */
int tm_isdst; /* 夏令时标识符,实行夏令时的时候,tm_isdst为正。不实行夏令时的进候,tm_isdst为0;不了解情况时,tm_isdst()为负。*/
};
#define _TM_DEFINED
#endif

需要特别注意的是,年份是从1900年起至今多少年,而不是直接存储如2011年,月份从0开始的,0表示一月,星期也是从0开始的, 0表示星期日,1表示星期一。

一般有两个函数来支持tm结构体:

1
2
struct tm * gmtime(const time_t *timer);                                          
struct tm * localtime(const time_t * timer);
  • 日历时间(Calendar Time)是通过time_t数据类型来表示的,用time_t表示的时间(日历时间)是从一个时间点(例如:1970年1月1日0时0分0秒)到此时的秒数。
  • gmtime()函数是将日历时间转化为世界标准时间(即格林尼治时间),并返回一个tm结构体来保存这个时间
  • localtime()函数是将日历时间转化为本地时间。比如现在用gmtime()函数获得的世界标准时间是2005年7月30日7点18分20秒,那么我用localtime()函数在中国地区获得的本地时间会比时间标准时间晚8个小时,即2005年7月30日15点18分20秒。

则一般的使用方式就是:

1
2
3
4
5
6
7
8
#include "time.h"
#include "stdio.h"

struct tm *local;//初始化tm结构体,这里是指针,因为localtime返回的是指针
time_t t;//初始化一个time_t
t=time(NULL);//使用time()函数获取日历时间
local = localtime(&t);//传入time_t的地址,获取当地时间
printf("Local hour is: %d\n",local->tm_hour);

现在开始写一个write函数,我们假设上层已经保存了day信息、定义了最大行数和当前行数,且打开了一个文件fp。

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
#include<mutex>
#include "time.h"
#include<stdarg>
#include<string>
#include<stdio.h>//fopen、fclose
#include<cassert>
using namespace std;
void write(int level, const char* format,...)
{
//初始化时间
struct tm *nowtime;
time_t t;
t = time(NULL);
nowtime = localtime(&t);

//在写之前看要不要创建新文件
//如果当前日期变了,一般来说判断day就可以了;或是行数已满,就换一个文件


//接下来涉及行数的改写,以及文件的切换,要互斥。因为实际上一个线程切换文件即可,如果有线程在切换其他线程不可动
unique_lock<mutex> locker(mux);
linecounts++;//先++,因为行数是从0开始的,++后刚好判断是不是满了,这个操作要互斥
if(logday != nowtime->tm_mday || linecounts == maxlines)//如果换了一天或行数满了
{
char newname[36];//用snprintf,不能用string了
if(logday != nowtime->tm_mday)//如果是换了一天
{
logday = nowtime->tm_mday;//修改天
linecounts = 0;//换文件了
filenum = 0;//文件份数从0开始
//为了格式化命名,要用format,这里用snprintf写入str
snprintf(newname, 36, "%d-%02d-%02d_log%05d.txt",nowtime->tm_year+1900, nowtime->tm_mon+1, nowtime->tm_mday, filenum);
}
else//行数满了
{
linecounts = 0;
filenum++;
snprintf(newname, 36, "%d-%02d-%02d_log%05d.txt",nowtime->tm_year+1900, nowtime->tm_mon+1, nowtime->tm_mday, filenum);
}

fflush(logfp);//在关闭文件前要把文件缓存区的内容写完
fclose(logfp);
logfp = fopen(newname,"w");
assert(logfp != nullptr);//创建失败报错
}
locker.unlock();

//开始写入,注意日志系统是单例的,如果还用到共享变量要锁,这里不用了
char infobuffer[128];//一般一行日志没那么长,128足够了
char timebuffer[36];//时间头
string allinfo;
//分级
switch(level)
{
case 0:
allinfo += "[debug]";
break;
case 1:
allinfo += "[info]";
break;
case 2:
allinfo += "[warning]";
break;
case 3:
allinfo += "[error]";
break;
default:
allinfo += "[info]";
break;
}
//添加时间信息
snprintf(timebuffer,36, "%d-%02d-%02d_%02d:%02d:%02d:",
nowtime->tm_year+1900, nowtime->tm_mon+1, nowtime->tm_mday,
nowtime->tm_hour,nowtime->tm_min,nowtime->tm_sec);//只精确到秒,更具体的信息交给内容体现

allinfo += string(timebuffer);

//写内容
va_list vaList;
va_start(vaList,format);
vsnprintf(infobuffer,128,format,vaList);
va_end(vaList);

allinfo += string(infobuffer)+"\n";//注意换个行

//分异步还是同步
//异步由于异步线程还没有创建,先不管,但也可以知道形式
/*
if(isAsync)
blockque.push(allinfo);
else
*/
fputs(allinfo.c_str(),logfp);//要互斥,这部分忘记了,日志系统处已发现并改正

}

对于异步线程的push,这里可以做一些思考:

  • 如果异步是像上面这种形式,那么工作线程可能会因为一个loginfo就阻塞,反倒不如直接写入;
  • 但如果要阻塞时不阻塞直接写入,就会导致时间顺序不对;
  • 一种解决方案是,直接再创建一个线程执行push,让线程阻塞;但这样的结果就是每个工作线程可能因为info又创建一个线程;
  • 这样的结果就是资源相当浪费(不是不可行,前面的close的操作也支持了这样的push),或许不如就让时间顺序不一致。即当阻塞队列满了就执行同步写(不过这样前面对close里push讨论的很多就没意义辣)
  • 或许最好的办法就是让阻塞队列长度和异步线程个数取得平衡,反正就是工作函数不要因为一个push阻塞了。

test

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
#include<iostream>
#include<mutex>
#include "time.h"
#include<stdarg.h>
#include<string>
#include<stdio.h>//fopen、fclose
#include<cassert>
using namespace std;

//初始化一些上层变量
const int maxlines = 256;
int linecounts = 0;
int logday = 123;
int filenum = 0;
mutex mux;
FILE *logfp = fopen("tmp.txt","w");


void write(int level, const char* format,...)
{
//初始化时间
struct tm *nowtime;
time_t t;
t = time(NULL);
nowtime = localtime(&t);

//在写之前看要不要创建新文件
//如果当前日期变了,一般来说判断day就可以了;或是行数已满,就换一个文件


//接下来涉及行数的改写,以及文件的切换,要互斥。因为实际上一个线程切换文件即可,如果有线程在切换其他线程不可动
unique_lock<mutex> locker(mux);
linecounts++;//先++,因为行数是从0开始的,++后刚好判断是不是满了,这个操作要互斥
if(logday != nowtime->tm_mday || linecounts == maxlines)//如果换了一天或行数满了
{
char newname[36];//用snprintf,不能用string了
if(logday != nowtime->tm_mday)//如果是换了一天
{
logday = nowtime->tm_mday;//修改天
linecounts = 0;//换文件了
filenum = 0;//文件份数从0开始
//为了格式化命名,要用format,这里用snprintf写入str
snprintf(newname, 36, "%d-%02d-%02d_log%05d.txt",nowtime->tm_year+1900, nowtime->tm_mon+1, nowtime->tm_mday, filenum);
}
else//行数满了
{
linecounts = 0;
filenum++;
snprintf(newname, 36, "%d-%02d-%02d_log%05d.txt",nowtime->tm_year+1900, nowtime->tm_mon+1, nowtime->tm_mday, filenum);
}

fflush(logfp);//在关闭文件前要把文件缓存区的内容写完
fclose(logfp);
logfp = fopen(newname,"w");
assert(logfp != nullptr);//创建失败报错
}
locker.unlock();

//开始写入,注意日志系统是单例的,如果还用到共享变量要锁,这里不用了
char infobuffer[128];//一般一行日志没那么长,128足够了
char timebuffer[36];//时间头
string allinfo;
//分级
switch(level)
{
case 0:
allinfo += "[debug]";
break;
case 1:
allinfo += "[info]";
break;
case 2:
allinfo += "[warning]";
break;
case 3:
allinfo += "[error]";
break;
default:
allinfo += "[info]";
break;
}
//添加时间信息
snprintf(timebuffer,36, "%d-%02d-%02d_%02d:%02d:%02d:",
nowtime->tm_year+1900, nowtime->tm_mon+1, nowtime->tm_mday,
nowtime->tm_hour,nowtime->tm_min,nowtime->tm_sec);//只精确到秒,更具体的信息交给内容体现

allinfo += string(timebuffer);

//写内容
va_list vaList;
va_start(vaList,format);
vsnprintf(infobuffer,128,format,vaList);
va_end(vaList);

allinfo += string(infobuffer)+"\n";//注意换个行

//分异步还是同步
//异步由于异步线程还没有创建,先不管,但也可以知道形式
/*
if(isAsync)
blockque.push(allinfo);
else
*/
fputs(allinfo.c_str(),logfp);//要互斥,这部分忘记了,日志系统处已发现并改正

}


int main()
{
int level;
for(int i=0;i<1024;i++)
{
level = i%4;
write(level,"hello, this is num [%d], for %s %d",i,"level",level);
}
fclose(logfp);
return 0;
}
1
2
3
sun2@ubuntu:~/Desktop/websever_test/logwrite$ g++ -std=c++14 -o write write_test.cpp//编译
sun2@ubuntu:~/Desktop/websever_test/logwrite$ ./write
//结果就不放了,产生了刚好四个文件(tmp不算了),内容都是正确的,就不截图了

日志系统log

上面的write其实有些bug,就是当调用write时,先判断要不要切换文件,再看是写还是放入阻塞队列。这对于同步写是对的,但对于异步写,可能异步线程还没写完一个文件,就被write函数切换了文件,这实际上是不对的,因为放入阻塞队列不代表写进文件了,这里再把write操作解耦,分同步异步。

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
//头文件
#ifndef LOG_H
#define LOG_H

#include<mutex>
#include "time.h"
#include<stdarg.h>
#include<string>
#include<stdio.h>
#include<cassert>
#include<thread>
#include <dirent.h> //opendir
#include <sys/stat.h> //mkdir
#include "blockqueue.h"
using namespace std;
class Log
{
private:
Log();//单例模式构造函数私有,成员函数才能调用构造函数
public:
Log(Log const&) = delete;
Log& operator=(Log const&) = delete;
~Log();//关闭...//析构函数实际上和构造函数一样,可以private,因为本质上是成员函数调用
static Log* instance();//单例

//函数声明和定义,只能有一个使用默认参数,如果函数的声明和定义是分开的,那缺省函数不能在函数声明和定义中同时出现
//默认参数在函数声明中提供,当又有声明又有定义时,定义中不允许默认参数(定义中的默认参数是无用的,必须传入参数才能找到匹配的函数)
void init(int level=1, const char* fpath = "./log",int maxqueue_size=1024,int threadnum=1);//不能用构造函数传参,使用一个init传参初始化

void setlevel(int level){loglevel = level;}//修改level的接口,只允许主线程修改,因此不用互斥
int getlevel(){return loglevel;}
bool isopen(){return logisopen;}//看是否打开日志的接口

void createthread(int threadnum);
static void logthread();//异步线程的回调函数,需要是staic,没有this隐藏参数
void write(int level, const char *format,...);//同步写,解耦
void close();
private:
void asyncwrite();//互斥写,不用lambda表达式,因为要用到log类的变量,并修改它们
void changefile(struct tm *nowtime);//write函数的解耦
struct tm* gettime();
private:
static const int maxlines = 52000;
FILE *logfp;
int linecounts;
int filenum;
const char* path;
int logday;
bool isasync;
bool logisopen;
int loglevel;
unique_ptr<blockqueue<string>> blockque;//不用lambda表达式可以用uniqueptr,因为一个指针一起用。用指针是因为要根据队列长度动态构造
mutex mux;
};

//我们想用一个函数封装write函数,比如logoinfo调用level1的write,并且还要能判断loglevel支不支持
//但函数封装变参函数,为了传递可变参数,实际上还要修改write的实现,不如用宏来实现,使用##__VA_ARGS__传递可变参数,让编译器把宏替换为真实的函数
//##__VA_ARGS__的优点是,对于宏调用,如果format是一个字符串也即后面没有可变参数,## 操作将使预处理器(preprocessor)去除掉它前面的那个逗号。
//宏与类无关了,这里必须isopen了才能使用
#define LOG_BASE(level, format, ...) \
do {\
Log* log = Log::instance();\
if (log->isopen() && log->getlevel() <= level) {\
log->write(level, format, ##__VA_ARGS__); \
}\
} while(0);

#define LOG_DEBUG(format, ...) do {LOG_BASE(0, format, ##__VA_ARGS__)} while(0);
#define LOG_INFO(format, ...) do {LOG_BASE(1, format, ##__VA_ARGS__)} while(0);
#define LOG_WARN(format, ...) do {LOG_BASE(2, format, ##__VA_ARGS__)} while(0);
#define LOG_ERROR(format, ...) do {LOG_BASE(3, format, ##__VA_ARGS__)} while(0);

#endif

关于为什么要用do-while(0)使用宏,主要是希望多语句宏函数在大部分时刻正确展开执行:(29条消息) 宏定义为什么要使用do{……}while(0)形式_土豆爸爸的博客-CSDN博客。宏的换行用\。

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
#include "log.h"
using namespace std;

Log* Log::instance()
{
static Log instance;//调用构造函数
return &instance;
}
struct tm* Log::gettime()
{
struct tm *nowtime;
time_t t;
t = time(NULL);
nowtime = localtime(&t);
return nowtime;
}

void Log::changefile(struct tm *nowtime)//完成行数增加、判断文件切换
{
//接下来涉及行数的改写,以及文件的切换,要互斥。因为实际上一个线程切换文件即可,如果有线程在切换其他线程不可动
//unique_lock<mutex> locker(mux);互斥交给上层,因为fputs也要互斥
linecounts++;//先++,因为行数是从0开始的,++后刚好判断是不是满了,这个操作要互斥
if(logday != nowtime->tm_mday || linecounts == maxlines)//如果换了一天或行数满了
{
char newname[48];//用snprintf,不能用string了
if(logday != nowtime->tm_mday)//如果是换了一天
{
logday = nowtime->tm_mday;//修改天
linecounts = 0;//换文件了
filenum = 0;//文件份数从0开始
//为了格式化命名,要用format,这里用snprintf写入str
snprintf(newname, 48, "%s/%d-%02d-%02d_log%05d.txt",//补充一个前缀-文件夹
path, nowtime->tm_year+1900, nowtime->tm_mon+1, nowtime->tm_mday, filenum);
}
else//行数满了
{
linecounts = 0;
filenum++;
snprintf(newname, 48, "%s/%d-%02d-%02d_log%05d.txt",
path, nowtime->tm_year+1900, nowtime->tm_mon+1, nowtime->tm_mday, filenum);
}

fflush(logfp);//在关闭文件前要把文件缓存区的内容写完
fclose(logfp);
logfp = fopen(newname,"w");
assert(logfp != nullptr);//创建失败报错
}
//locker.unlock();
}

void Log::write(int level, const char *format,...)
{
//初始化时间
struct tm *nowtime = gettime();

//-----------------根据传入的信息整理成一行日志-------------------------
char infobuffer[128];//一般一行日志没那么长,128足够了
char timebuffer[36];//时间头
string allinfo;
//分级
switch(level)
{
case 0:
allinfo += "[debug]";
break;
case 1:
allinfo += "[info]";
break;
case 2:
allinfo += "[warning]";
break;
case 3:
allinfo += "[error]";
break;
default:
allinfo += "[info]";
break;
}
//添加时间信息
snprintf(timebuffer,36, "%d-%02d-%02d_%02d:%02d:%02d:",
nowtime->tm_year+1900, nowtime->tm_mon+1, nowtime->tm_mday,
nowtime->tm_hour,nowtime->tm_min,nowtime->tm_sec);//只精确到秒,更具体的信息交给内容体现

allinfo += string(timebuffer);

//写内容
va_list vaList;
va_start(vaList,format);
vsnprintf(infobuffer,128,format,vaList);
va_end(vaList);

allinfo += string(infobuffer)+"\n";//注意换个行
//----------------------------------------------------------------------------------------------

//分异步还是同步,要不要切换文件交给异步线程判断
if(isasync && !blockque->full())
blockque->push(allinfo);//异步直接插入
else
{
//在写之前看要不要创建新文件
//如果当前日期变了,一般来说判断day就可以了;或是行数已满,就换一个文件

lock_guard<mutex> locker(mux);//互斥
changefile(nowtime);//直接交给该函数完成
fputs(allinfo.c_str(),logfp);//操作文件缓冲区,也要互斥
fflush(logfp);
}
}

Log::Log()//初始化一部分变量
{
//初始行数为-1,因为是先++然后判断再写入,初始为0的话,第一份文件会少一行,
//比如最大行为2,初始为0;则++,写入;++就换文件了,只写了一行,所以初始要是-1。换文件后置为0
//因为换文件后没++了,写了一行,就是正确的
linecounts = -1;
filenum = 0;
isasync = false;
blockque = nullptr;
logday = 0;
logfp = nullptr;
logisopen = false;//init才算打开
}
void Log::close()
{

logisopen = false;
if(isasync)//异步的话要让线程退出
{
while(!blockque->empty());//等待工作完成

blockque->close();
}
if(logfp)//如果打开了文件要关闭
{
//由于其他线程可能正在使用,因此要等待互斥锁
lock_guard<mutex> locker(mux);
fflush(logfp);//刷新缓冲区
fclose(logfp);
logfp = nullptr;
}
}
Log::~Log()
{
close();
}

void Log::logthread()//异步线程回调函数
{
Log::instance()->asyncwrite();//调用类成员函数
}

void Log::init(int level, const char* fpath,int maxqueue_size,int threadnum)
{
if(logisopen == true)
return;//只允许init一次
logisopen = true;
loglevel = level;
if(maxqueue_size>0)//有阻塞队列则异步
{
isasync = true;
//创建阻塞队列
unique_ptr<blockqueue<string>> que(new blockqueue<string>(maxqueue_size));
blockque = move(que);//移动赋值
createthread(threadnum);
}
else
isasync = false;



//初始化时间
struct tm *nowtime;
time_t t;
t = time(NULL);
nowtime = localtime(&t);

logday = nowtime->tm_mday;
path = fpath;

char filename[48];//用snprintf,不能用string了
snprintf(filename, 48, "%s/%d-%02d-%02d_log%05d.txt",//补充一个前缀-文件夹
path, nowtime->tm_year+1900, nowtime->tm_mon+1, nowtime->tm_mday, filenum);

//初步打开文件,没有文件夹就创建文件夹
if(opendir(path) == NULL)//如果文件夹不存在
mkdir(path,0777);//0777是最大的访问权

logfp = fopen(filename,"w");
assert(logfp!=nullptr);
}

void Log::asyncwrite()
{
string str;
while(blockque->pop(str))
{
struct tm* nowtime = gettime();
lock_guard<mutex> locker(mux);//互斥
changefile(nowtime);//每写一行判断要不要换文件
fputs(str.c_str(),logfp);
fflush(logfp);
}
}

void Log::createthread(int threadnum)
{
for(int i=0;i<threadnum;i++)
{
thread(logthread).detach();//因为内部函数采用单例调用,logthread不用传入this指针
}
}

test

所有的都准备完成了,这里分同步和异步测试一下

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
//同步
#include<iostream>
#include "log.h"
#include<thread>
#include <pthread.h>
#include <unistd.h>//使用sleep
using namespace std;
int main()
{
cout<<"----------同步测试------------------"<<endl;
Log::instance()->init(0,"./同步log",0,0);

//测试时先把日志最大行数改成100
thread([]{
for(int i=0;i<70;i++)
{
LOG_DEBUG("num %d debug",i);
sleep(1);
}
}).detach();

thread([]{

for(int i=0;i<70;i++)
{
LOG_INFO("num %d info",i);
sleep(1);
}
}).detach();

thread([]{
for(int i=0;i<20;i++)
{
LOG_ERROR("num %d ERROR",i);
sleep(1);
}
}).detach();

thread([]{
for(int i=0;i<70;i++)
{
LOG_WARN("num %d warn",i);
sleep(1);
}
}).detach();

cout<<"------主线程退出---------"<<endl;

pthread_exit(NULL);
return 0;
}

1
2
3
4
5
6
//注意,编译时要把log.cpp手动链接进来,它们是独立的文件
sun2@ubuntu:~/Desktop/websever_test/log$ g++ -std=c++14 -o test test.cpp log.cpp -lpthread
sun2@ubuntu:~/Desktop/websever_test/log$ ./test
----------同步测试------------------
------主线程退出---------
//结果很ok
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
//异步测试
#include<iostream>
#include "log.h"
#include<thread>
#include <pthread.h>
#include <unistd.h>//使用sleep
using namespace std;
int main()
{
cout<<"----------异步测试------------------"<<endl;
Log::instance()->init(0,"./异步log",100,2);

//测试时先把日志最大行数改成100
thread([]{
for(int i=0;i<70;i++)
{
LOG_DEBUG("num %d debug",i);
sleep(1);
}
}).detach();

thread([]{

for(int i=0;i<70;i++)
{
LOG_INFO("num %d info",i);
sleep(1);
}
}).detach();

thread([]{
for(int i=0;i<20;i++)
{
LOG_ERROR("num %d ERROR",i);
sleep(1);
}
}).detach();

thread([]{
for(int i=0;i<70;i++)
{
LOG_WARN("num %d warn",i);
sleep(1);
}
}).detach();

cout<<"------主线程退出---------"<<endl;

pthread_exit(NULL);
return 0;
}

为了更有测试效果,我们在asyncwrite函数里加一个打印

1
2
3
4
5
6
7
8
9
10
11
12
void Log::asyncwrite()
{
string str;
while(blockque->pop(str))
{
cout<<"async doing: "<<str<<endl;//打印
struct tm* nowtime = gettime();
lock_guard<mutex> locker(mux);//互斥
changefile(nowtime);//每写一行判断要不要换文件
fputs(str.c_str(),logfp);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sun2@ubuntu:~/Desktop/websever_test/log$ g++ -std=c++14 -o test2 test2.cpp log.cpp  -lpthread
sun2@ubuntu:~/Desktop/websever_test/log$ ./test2
----------异步测试------------------
------主线程退出---------
async doing: [debug]2022-10-04_06:41:07:num 0 debug

async doing: [info]2022-10-04_06:41:07:num 0 info

async doing: [warning]2022-10-04_06:41:07:num 0 warn

async doing: [error]2022-10-04_06:41:07:num 0 ERROR

async doing: [debug]2022-10-04_06:41:08:num 1 debug

async doing: [info]2022-10-04_06:41:08:num 1 info

...

异步测试效果还是不错的,但是最后一个文件因为日志系统没有析构,导致缓冲区没有flush,所以最后一个文件没有写入,并且异步线程也不会自动退出。现在我们再修改一下test文件。注:现在已经改为每写一行fflush一次,因为当logerror时上层往往随后会终止程序,要记录必须刷新。

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
//异步测试
#include<iostream>
#include "log.h"
#include<thread>
#include <pthread.h>
#include <unistd.h>//使用sleep
using namespace std;
int main()
{
cout<<"----------异步测试------------------"<<endl;
Log::instance()->init(0,"./异步log",100,2);

//测试时先把日志最大行数改成100
thread([]{
for(int i=0;i<70000;i++)
{
LOG_DEBUG("num %d debug",i);
//sleep(1);
}
}).detach();

thread([]{

for(int i=0;i<70000;i++)
{
LOG_INFO("num %d info",i);
//sleep(1);
}
}).detach();

thread([]{
for(int i=0;i<20000;i++)
{
LOG_ERROR("num %d ERROR",i);
//sleep(1);
}
}).detach();

thread([]{
for(int i=0;i<70000;i++)
{
LOG_WARN("num %d warn",i);
//sleep(1);
}
}).detach();
sleep(1);
Log::instance()->close();
cout<<"------主线程退出---------"<<endl;

pthread_exit(NULL);
return 0;
}

这里让生产者线程快速产生大量的任务,让异步线程工作,然后等一段时间(主要是让生产者线程产生完工作,不至于调用时阻塞队列还是空的),再手动调用close。现在,所有的文件都写成功,并且异步线程退出成功。

如果让每个异步线程写一行都停一会(sleep(1)),使得在调用close后任务还没写完,可以发现close会等待异步线程把任务做完,说明关闭日志系统很成功。

关于close和析构

对于阻塞队列和日志系统,都使用了析构函数调用close,实际上日志系统的close函数是我新增的,因为我需要手动在程序里close,没办法让主线程退出时而异步线程还存在时析构而调用close。这是因为:

日志系统的static变量,只有当程序全部退出才会析构。

这会导致一些问题,就是手动close之后,主线程和异步线程都退出了会导致析构再调用一次close。在阻塞队列里,两次close不会导致什么问题,但在日志系统会有问题:free(): double free detected in tcache 2,即释放已释放的资源。在close函数中,原本没有这一行:logfp = nullptr;,而fclose并不会把fp置为nullptr,那么第二次close函数的if(logfp)就成立,又调用一次fclose,导致报错。

这里引起了一些思考:在服务器运行的时候,没有什么办法让服务器主动退出调用析构函数,ctrl+c会直接终止进程。在最初版本的tinywebsever中是捕获了ctrl+c的信号,处理成stop的一个flag通知服务器结束,c++11的版本直接不能通知服务器停下,必须强行终止。

因此,不如都手动close,而析构函数不做处理。可以使用一个线程接收终止输入,需要终止时就让这个线程调用一系列的close函数,让其他线程退出,之后调用析构函数就不会产生二次close的冲突。

这是否会浪费RAII带来的作用呢?

注意,我们手动调用close只是为了让线程不会阻塞,因为线程存在时又不会析构,不析构就没办法close然后就矛盾了。对于不会引起阻塞的类,还是可以放在析构函数里的。比如日志系统就需要手动调用close,这可能会导致一些其他资源需要在日志系统前关闭,那就导致这些资源也需要在日志系统close前手动close。

数据库

头文件#include <mysql/mysql.h>

数据库的一个连接句柄的初始化有三步

  • 定义一个sql指针:MYSQL *sql = nullptr;
  • 用这个指针初始化一个sql结构体,返回一个指向这个结构体的指针:sql = mysql_init(sql);
  • init后就connect,连接数据库,返回一个可用连接sql = mysql_real_connect(sql, host,user, pwd,dbName, port, unix_socket, client_flag);
    • host是主机名或IP,如果“host”是NULL或字符串”localhost”,连接将被视为与本地主机的连接。
    • 如果unix_socket不是NULL,该字符串描述了应使用的套接字或命名管道。注意,“host”参数决定了连接的类型。
    • client_flag的值通常为0,其他标志可以实现特定的功能

然后这个sql句柄就可以用来执行语句了,最后在不使用的时候还需要调用mysql_close(sql);释放连接。并且,为了避免在使用库完成应用程序后发生内存泄漏(例如,在关闭与服务器的连接之后),可以显式调用mysql_library_end()。这样可以执行内存 Management 以清理和释放库使用的资源。

上面就是一个连接的建立,对于多个连接,我们可以把多个连接初始化后放入一个队列里,这样就构成了一个连接池。对于这样一个共享的连接池,就需要互斥操作。而这一个队列又和阻塞队列不同,队列是可能空的但不可能会因为满而阻塞——可用的连接是一定的,push回去不会多。在阻塞队列中使用了两个条件变量管理了空/满缓冲区的阻塞,这里只需要管理空,也即pop操作的阻塞。可以用一个条件变量,也可以用一个信号量。两种方法都比较简单,不过为了熟悉一下条件变量,还是使用条件变量(信号量就使用sem_post(&semId_)、sem_wait(&semId_)、sem_init(&semId_, 0, MAX_CONN_))。

当上层需要登录或注册时,会尝试获取一个连接,然后使用完后释放连接。这里的问题是,当没有连接可用时,是阻塞等待还是直接返回错误。或许使用折中会好一点,即用cond.wait_for()阻塞一段时间。当释放一个连接后就尝试唤醒一个阻塞的线程。

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
#ifndef SQLCONNPOOL_H
#define SQLCONNPOOL_H

#include <mysql/mysql.h>
#include <mutex>
#include <queue>
#include <condition_variable>
using namespace std;
class Sqlconnpool
{
private:
mutex mux;
condition_variable cond;
queue<MYSQL*> connque;
int maxconn;
int freecount;
Sqlconnpool();
~Sqlconnpool(){}//析构函数实际上和构造函数一样,可以private,因为本质上是成员函数调用
public:
Sqlconnpool(const Sqlconnpool&) = delete;
Sqlconnpool& operator=(const Sqlconnpool&) = delete;

void close();
static Sqlconnpool* instance();
MYSQL* getconn(int timeout);
void freeconn(MYSQL* conn);
//无法使用构造函数传参,用init,默认参数写声明中
void init(const char* host,int port,const char* user,const char* pwd,const char* dbname,int connsize=10);
int conncount();

};

#endif
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
#include "sqlconnpool.h"
#include <chrono>
#include <cassert>
#include "log.h"
using namespace std;
Sqlconnpool::Sqlconnpool()
{
maxconn = 0;
freecount = 0;
}

Sqlconnpool* Sqlconnpool::instance()
{
static Sqlconnpool instance;
return &instance;
}

MYSQL* Sqlconnpool::getconn(int timeout = 0)
{
assert(timeout>=0);//
unique_lock<mutex> locker(mux);
while(connque.empty())
if(cond.wait_for(locker, chrono::seconds(timeout)) == std::cv_status::timeout)//超时
{
LOG_WARN("Sqlconnpool busy")
return nullptr;
}

MYSQL* sql = connque.front();
connque.pop();
freecount--;
return sql;
}

void Sqlconnpool::freeconn(MYSQL* conn)
{
assert(conn);//防止放入nullptr
lock_guard<mutex> locker(mux);
connque.push(conn);
freecount++;
cond.notify_one();//唤醒一个get线程
}
int Sqlconnpool::conncount()
{
lock_guard<mutex> locker(mux);
return maxconn-freecount;
}

void Sqlconnpool::init(const char* host,int port,const char* user,const char* pwd,const char* dbname,int connsize)
{
assert(connsize>0);
maxconn = connsize;
freecount = connsize;
for(int i=0;i<maxconn;i++)
{
//三步初始化
MYSQL* sql = nullptr;
sql = mysql_init(sql);
if(!sql)
{
LOG_ERROR("sql number %d init error",i);
assert(sql);//终止报错
}
sql = mysql_real_connect(sql,host,user,pwd,dbname,port,nullptr,0);
if(!sql)
{
LOG_ERROR("sql number %d connect error",i);
assert(sql);//终止报错
}
connque.push(sql);//放入的一定不是nullptr
}
}

void Sqlconnpool::close()
{
unique_lock<mutex> locker(mux);
while(freecount != maxconn)//必须要等待所有连接都放回来,直接close再执行查询程序会崩溃
cond.wait(locker);//每放回一个连接唤醒一次,然后判断

while(!connque.empty())//逐个关闭连接
{
mysql_close(connque.front());
connque.pop();
}
mysql_library_end();//释放库的资源
}
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
#ifndef SQLRAII_H
#define SQLRAII_H

#include "sqlconnpool.h"
#include <cassert>
using namespace std;
class SqlRAII
{
private:
MYSQL* conn;//保存连接好的sql
Sqlconnpool* connpool;//保存连接池
public:
SqlRAII(MYSQL** sql,Sqlconnpool* sqlpool,int timeout = 0)//传入sql指针的地址,即&sql,获取连接后传出去
{
assert(sqlpool);//必须先建好连接池

*sql = sqlpool->getconn(timeout);//可能会超时
//为了用户自行getconn和使用sqlraii的统一,这里统一让用户在上层处理sql为nullptr的情况
conn = *sql;
connpool = sqlpool;
}
~SqlRAII()
{
if(conn)//有连接就释放
connpool->freeconn(conn);
}
};

#endif

test

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
#include <thread>
#include "sqlraii.h"
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "threadpool.h"
#include "log.h"
#include <mysql/mysql.h>
using namespace std;

void task( Sqlconnpool* sqlpool,int i)
{
MYSQL* sql;
//SqlRAII(&sql,sqlpool,0);这个会出错,下面介绍
SqlRAII myconn(&sql,sqlpool,0);//这样才行
if(sql==nullptr)
{
cout<<"haha, thread "<<i<<" can't get the connection\n";
}
else
{
cout<<"haha, thread "<<i<<" gets the connection\n";
sleep(i);
MYSQL_FIELD *fields = nullptr;
MYSQL_RES *res = nullptr;
const char* order = "SELECT username, passwd FROM user";//命令不用加分号
if(mysql_query(sql,order))
{
cout<<"query error\n";
return;
}
res = mysql_store_result(sql);//存储完整的结果集
int j = mysql_num_fields(res);//获取列数
fields = mysql_fetch_fields(res);//返回所有字段结构的数组
cout<<"thread "<<i<<":";
for(int k=0;k<j;k++)
cout<<fields[k].name<<" ";//输出列名
cout<<endl;
while(MYSQL_ROW row = mysql_fetch_row(res))
{
for(int k=0;k<j;k++)
cout<<row[k]<<" ";
cout<<endl;
}
mysql_free_result(res);//释放结果集
}
}

int main()
{
Sqlconnpool* sqlpool = Sqlconnpool::instance();
Log::instance()->init(0,"./log",10,1);
sqlpool->init("localhost",3306,"root","Qq1424277869!","myWebSever");
cout<<"sqlconnpool init successfully!"<<endl;
//调线程池
threadpool threadp(20);
for(int i=3;i<23;i++)
threadp.addTask(bind(task,sqlpool,i));//按序放入,其实就前十个能取得连接
cout<<"add task successfully!"<<endl;
sleep(1);//老样子,在调用close前等一下线程的工作初始化
//手动调用close
threadp.close();//通知任务做完自己退出,不会阻塞,注意封装了一层close。
sqlpool->close();//等待所有连接放回,因为要逐个关闭连接,会阻塞
Log::instance()->close();//日志一般最后关闭
cout<<"quit----------------"<<endl;
pthread_exit(NULL);//线程池的析构函数

}

这里用到了线程池的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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//最终版threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include<mutex>
#include<thread>
#include<condition_variable>
#include<functional>
#include<queue>
#include <cassert>//使用assert函数
class threadpool
{
private:
struct pool//封装三个资源
{
std::mutex mtx;//互斥锁
std::queue<std::function<void()>> taskQueue;//任务队列,无参数的function,调用时不用传参
std::condition_variable cond;//条件变量
bool isclose = false;//默认值是false
};
std::shared_ptr<pool> pool_;//共享指针,pool_是一个指针指向pool结构体,这个指针用于线程池操作资源

public:
threadpool(int threadnum = 8):pool_(std::make_shared<pool>())//以make_shared的方式new一个对象给pool_指针
{
assert(threadnum > 0);//没有线程就报错
for(int i=0;i<threadnum;i++)//创建线程池
std::thread([pool_t = pool_]{//现在要按值捕获,相当于拷贝构造共享指针,计数+1,且指向相同内容
std::unique_lock<std::mutex> locker(pool_t->mtx);//定义一个locker对象,现在已经锁住了
while(true)
{
if(!pool_t->taskQueue.empty())//如果有任务
{
auto task = pool_t->taskQueue.front();
pool_t->taskQueue.pop();
locker.unlock();
//解锁后再执行
task();
//执行完了,进入下一轮循环,注意要锁住
locker.lock();//抢占锁
}
else if(pool_t->isclose)
break;
else//如果没有任务
pool_t->cond.wait(locker);//解锁并等待,唤醒后会抢占互斥锁
}
}).detach();//把thread分离,不用手动join,结束自动回收
}

void addTask(std::function<void()> task)
{
std::lock_guard<std::mutex> locker(pool_->mtx);//定义一个locker对象
pool_->taskQueue.emplace(task);//这种方式,使用emplace和push没啥区别,task本身就是临时对象
//如果要真正使用到emplace调用构造函数,还要配合std::forward完美转发,此时无论构造函数是不是explicit(不能隐式转换),都可以正常工作
pool_->cond.notify_one();//插入一个元素唤醒一个线程
}
void close()
{
pool_->isclose = true;
pool_->cond.notify_all();
}
~threadpool()//析构函数
{
}
};
#endif
  • 为什么说SqlRAII(&sql,sqlpool,0);会出错呢,主要是临时变量作用域的问题,这条语句会给sql赋值,但是临时变量只存活于这条语句里,然后就析构了,会把sql再放回连接池,其他的线程无论多少都能再拿到这个sql连接句柄。
  • 这样,多个用户可能同时操作一个句柄,可能会引发问题;并且当调用连接池的close函数时,总是会发现连接池是满了,直接把所有的连接都关闭,这样在关闭后执行查询等操作就会出错,直接导致程序崩溃。因此要用一个有名变量,作用于线程的存活空间中。

编译一下,执行发现挺正常的,日志系统也正常,有一半能获取连接,一半不能获取连接。

1
g++ -std=c++14 -o test test.cpp log.cpp sqlconnpool.cpp -lpthread `mysql_config --cflags --libs`//要链接mysql库

我们改变一下,现在设置成可以超时10秒,可以发现这十秒钟内,在前面线程执行完放回连接后,剩下的线程又可以获取连接了。只有3个线程不能获取连接。则超时的设置也测试成功。

1
SqlRAII myconn(&sql,sqlpool,10);

结果为:

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
sun2@ubuntu:~/Desktop/websever_test/sqlpool$ g++ -std=c++14 -o test test.cpp log.cpp sqlconnpool.cpp -lpthread `mysql_config --cflags --libs`
sun2@ubuntu:~/Desktop/websever_test/sqlpool$ ./test
sqlconnpool init successfully!
add task successfully!
haha, thread 3 gets the connection
haha, thread 4 gets the connection
haha, thread 6 gets the connection
haha, thread 5 gets the connection
haha, thread 7 gets the connection
haha, thread 8 gets the connection
haha, thread 9 gets the connection
haha, thread 10 gets the connection
haha, thread 11 gets the connection
haha, thread 12 gets the connection
thread 3:username passwd
name passwd
jysama jysama
woshinidie cjy
haha, thread 13 gets the connection
thread 4:username passwd
name passwd
jysama jysama
woshinidie cjy
haha, thread 14 gets the connection
......

socket

socket-Linux

ping测试

测试终端之间的网络有没有联通。

这里先测试一下两台机器能不能互相ping通,首先是两台虚拟机之间:

image-20221008161104460

如果图片加载失败可以看文字描述:具体的,使用ifconfig查看虚拟机ip,除了本地host还有一块网卡,另一块网卡ens33内容的inet 192.168.248.131就是ip。然后两个虚拟机互相ping ip就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sun2@ubuntu:~/Desktop$ ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.248.131 netmask 255.255.255.0 broadcast 192.168.248.255//inet 后是ip
inet6 fe80::e754:7748:53d4:6f8e prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:1a:ab:67 txqueuelen 1000 (Ethernet)
RX packets 154 bytes 24552 (24.5 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 156 bytes 16227 (16.2 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0//本地local
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 175 bytes 14698 (14.6 KB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 175 bytes 14698 (14.6 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
/*
关于lo网卡:
是一个虚拟的网络接口,并没有对应的物理网卡,我们知道它的地址是 127.0.0.1 ,主要作为本地地址使用。 在程序开发中,我们常常把服务启动在这个地址上,通过浏览器来访问 127.0.0.1 或其解析的 localhost 来访问本地的服务进行调试。
*/

至于这里的ip,实际上使用了NAT转换共享了host主机的ip。虚拟机的网络设置有三种模式,可以参考:VMware Network Adapter VMnet1/8详解 - larryle - 博客园 (cnblogs.com)

其中NAT转换使用了vmnet8,在host主机的cmd中输入ipconfig,可以看到vmnet8的网段,以及本机的ip:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//展示部分内容
以太网适配器 VMware Network Adapter VMnet8:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::21ea:6a23:d725:a9b7%16
IPv4 地址 . . . . . . . . . . . . : 192.168.248.1//vmnet8,使用NAT转换的虚拟机的网关
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :

无线局域网适配器 WLAN:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::b835:29a:672:3928%12
IPv4 地址 . . . . . . . . . . . . : 192.168.31.213//本机ip
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.31.1//网关

现在我们试一下host主机和虚拟机的ping。

image-20221008162027010

也没什么问题。

Linux下套接字api简单介绍

1
2
3
4
5
6
7
//常用头文件
<sys/types.h> //primitive system data types(包含很多类型重定义,如pid_t、int8_t等)
<sys/socket.h> //与套接字相关的函数声明和结构体定义,如socket()、bind()、connect()及struct sockaddr的定义等
<sys/ioctl.h> //I/O控制操作相关的函数声明,如ioctl()
<stdlib.h> //某些结构体定义和宏定义,如EXIT_FAILURE、EXIT_SUCCESS等
<netdb.h> //某些结构体定义、宏定义和函数声明,如struct hostent、struct servent、gethostbyname()、gethostbyaddr()、herror()等
<netinet/in.h> //某些结构体声明、宏定义,如struct sockaddr_in、PROTO_ICMP、INADDR_ANY等
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
//常用函数
socket()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int socket(int domain, int type, int protocol)
domain: 协议类型,一般为AF_INET
type: socket类型
protocol:用来指定socket所使用的传输协议编号,通常设为0即可

bind()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
sockfd: socket描述符
my_addr:是一个指向包含有本机ip地址和端口号等信息的sockaddr类型的指针
addrlen:常被设为sizeof(struct sockaddr)
返回值:若成功则为0,若出错则为-1

connect()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen)
sockfd: 目的服务器的socket描述符
serv_addr:包含目的机器ip地址和端口号的指针
addrlen:sizeof(struct sockaddr)

listen()
头文件:
#include <sys/socket.h>
函数原型:
int listen(int sockfd, int backlog);
sockfd:socket()系统调用返回的socket描述符
backlog:指定在请求队列中的最大请求数,进入的连接请求将在队列中等待accept()它们。

accept()
头文件:
#include <sys/types.h>
#inlcude <sys/socket.h>
函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
sockfd:是被监听的socket描述符
addr:通常是一个指向sockaddr_in变量的指针,该变量用来存放提出连接请求服务的主机的信息
addrlen:sizeof(struct sockaddr_in)
成功时,返回非负整数,该整数是接收到套接字的描述符;出错时,返回-1,相应地设定全局变量errno。

send()
头文件:
#include <sys/socket.h>
函数原型:
int send(int sockfd, const void *msg, int len, int flags);
sockfd:用来传输数据的socket描述符
msg:要发送数据的指针
flags: 0

recv()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int recv(int sockfd, void *buf, int len, unsigned int flags)
sockfd:接收数据的socket描述符
buf:存放数据的缓冲区
len:缓冲的长度
flags:0

sendto()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);


recvfrom()
头文件:
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int fromlen)


read() write()
头文件:
#include <unistd.h>
int read(int fd, char *buf, int len)
int write(int fd, char *buf, int len)

int close(int sockfd);
#include<unistd.h>
关闭已连接的套接字只是导致相应描述符的引用计数减1,如果引用计数扔大于0,这个close调用并不会让TCP连接上发送一个FIN。
如果确实想发送一个FIN,可以用shutdown函数。

shutdown()
int shutdown(int sockfd, int how)
该函数的行为依赖howto参数的值:

SHUT_RD
套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。

SHUT_WR
对于TCP套接字,称为半关闭(half-close)。当前在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。(不管套接字的引用计数是否等于0)

SHUT_RDWR
等于调用shutdown函数两次,连接的读半部和写半部都关闭。
  • socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。
  • bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
  • 通过 listen() 函数可以让套接字进入被动监听状态,所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。所以,执行accept的是被动套接字,执行connect的是主动套接字。
  • 作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
  • 当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,大家注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
  • 两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。
  • recv函数和send函数提供了read和write函数一样的功能,不同的是他们提供了四个参数。前面的三个参数和read、write函数是一样的。
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
//结构体
//sockaddr在头文件#include <sys/socket.h>中定义,sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了
struct sockaddr {
sa_family_t sin_family;//地址族
   char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
   };
//sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
addr.sin_len=sizeof(addr);//socket字节长度
sin_family指代协议族,在socket编程中一般是AF_INET
sin_port存储端口号(使用网络字节顺序)
sin_addr存储IP地址,使用in_addr这个数据结构
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
/*
#define AF_UNIX 1 //local to host (pipes, portals)
#define AF_INET 2 //internetwork: UDP, TCP, etc.
...
#define AF_ATM 22 // Native ATM Services
#define AF_INET6 23 // Internetwork Version 6
*/

//in_addr,头文件#include <arpa/inet.h>
struct in_addr {
in_addr_t s_addr;
};
结构体in_addr 用来表示一个32位的IPv4地址
in_addr_t 一般为 32位的unsigned int,其字节顺序为网络顺序(network byte ordered),即该无符号整数采用大端字节序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <arpa/inet.h> 
uint16_t htons(uint16_t hostshort); //n指network
//将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian)
htons 是把你机器上的整数转换成“网络字节序”, 网络字节序是 big-endian,也就是整数的高位字节存放在内存的低地址处。
而我们常用的 x86 CPU (intel, AMD) 电脑是 little-endian,也就是整数的低位字节放在内存的低字节处。
举个例子:
假定你的port是0x1234,在网络字节序里,这个port放到内存中就应该显示成  
addr 0x12 //12字节是高位,放低地址
addr+1  0x34  
而在x86电脑上,0x1234放到内存中实际是:  
addr 0x34
addr+1 0x12 
htons 的用处就是把实际内存中的整数存放方式调整成“网络字节序”的方式。
//为了程序可扩展性,不管电脑是何种方式对齐,都使用这个函数
ntohs相反
1
2
3
4
#include <arpa/inet.h>  
uint32_t htonl(uint32_t hostlong); 
//同htons,本函数将一个32位数从主机字节顺序转换成网络字节顺序。
ntohl相反
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <arpa/inet.h>  
in_addr_t inet_addr(const char *cp);
将一个点分十进制的IP转换成一个长整数型数(u_long类型),也即in_addr结构体中s_addr的类型(大端排列)

inet_addr的参数是字符串,返回值是网络字节序,htonl的参数是32bit的ip,并且是主机字节序

//n指network
int inet_aton(const char *strptr, struct in_addr *addrptr); //将字符串ip转换成无符号长整型,并转换成网络字节序
inet_addr("*.*.*.*") //将字符串ip转换成无符号长整型(unsigned long int),并转换成网络字节序。

inet_addr与inet_aton不同在于,他的返回值为转换后的32位网络字节序二进制值,而不是作为出参返回,这样存在一个问题,他的返回值返回的有效IP地址为0.0.0.0255.255.255.255,如果函数出错,返回常量值INADDR_NONE(这个值一般为一个32位均为1的值),这意味着点分二进制数串255.255.255.255(IPv4的有限广播地址)不能由此函数进行处理。

inet_ntoa是inet_aton和(几乎和)inet_addr相反的函数
char FAR* inet_ntoa(
struct in_addr in
);

inet_pton //将点分十进制数ip地址转换陈32位二进制网络地址
inet_ntop //将32位二进制ip地址转换为点分十进制ip地址
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
socketfd 描述了一个 socket结构体 socket  结构体的定义如下:   
struct socket
{
socket_state state;
unsigned long flags;
const struct proto_ops *ops;
struct fasync_struct *fasync_list;
struct file *file;
struct sock *sk;
wait_queue_head_t wait;
short type;
};
其中,struct sock 包含有一个 sock_common 结构体,而sock_common结构体又包含有struct inet_sock 结构体,而struct inet_sock 结构体的部分定义如下:
struct inet_sock
{
struct sock sk;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
struct ipv6_pinfo *pinet6;
#endif
__u32 daddr; //IPv4的目的地址。
__u32 rcv_saddr; //IPv4的本地接收地址。
__u16 dport; //目的端口。
__u16 num; //本地端口(主机字节序)。

…………
}
由此,我们清楚了,socket结构体不仅仅记录了本地的IP和端口号,还记录了目的IP和端口(四元组)。
这样,通过一个socket描述符,就能accept和connect了。
而服务器一般只需要一个端口,即使accept也不会新开端口:
由于TCP/IP协议栈是维护着一个接收和发送缓冲区的。在接收到来自客户端的数据包后,服务器端的TCP/IP协议栈应该会做如下处理:如果收到的是请求连接的数据包(connect),则传给监听着连接请求端口的socetfd套接字,进行accept处理;
如果是已经建立过连接后的客户端数据包,则将数据放入接收缓冲区。这样,当服务器端需要读取指定客户端的数据时,则可以利用socketfd_new 套接字通过recv或者read函数到缓冲区里面去取指定的数据(因为socketfd_new代表的socket对象记录了客户端IP和端口,因此可以鉴别)。
本质上因为客户端的ip和端口不同,accept创建新的socketfd,可以通过socket区分用户

api详解

  • int socket(int domain, int type, int protocol);

    • socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

    • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

    • type:指定socket类型。常用的socket类型有,SOCK_STREAM(流式套接字,TCP)、SOCK_DGRAM(数据报式套接字,UDP)、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等

    • protocol:就是指定协议。常用的协议有,IPPROTO_TCP、PPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

      • ```c++
        #define IPPROTO_IP 0 /* dummy for IP /
        #define IPPROTO_ICMP 1 /
        control message protocol /
        #define IPPROTO_IGMP 2 /
        internet group management protocol /
        #define IPPROTO_GGP 3 /
        gateway^2 (deprecated) /
        #define IPPROTO_TCP 6 /
        tcp /
        #define IPPROTO_PUP 12 /
        pup /
        #define IPPROTO_UDP 17 /
        user datagram protocol /
        #define IPPROTO_IDP 22 /
        xns idp /
        #define IPPROTO_ND 77 /
        UNOFFICIAL net disk proto /
        #define IPPROTO_RAW 255 /
        raw IP packet */
        #define IPPROTO_MAX 256
        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

        * 有的程序protocol参数填的是0,有些填写的是IPPROTO_UDP或IPPROTO_TCP。如果type已经指明SOCK_STREAM或SOCK_DGRAM,protocol可以是0。type=SOCK_STREAM时,默认protocol就是IPPROTO_TCP;type=SOCK_DGRAM时,默认protocol就是IPPROTO_UDP。参数0即表示了默认传输。但是,最好还是指明使用哪种传输,因为types对应多种类型,比如数据流类型的有:atm,tcp等协议。

        * int **listen**(int fd, int backlog);

        * **sockfd** 一个已绑定未被连接的套接字描述符
        * **backlog** 连接请求队列的最大长度(一般由2到4)。用SOMAXCONN则为系统给出的最大值
        * 返回:若成功则为0,若出错则为-1
        * 执行listen 之后套接字进入被动模式。队列满了以后,将拒绝新的连接请求。客户端将出现连接错误WSAECONNREFUSED。

        * int **recv**(SOCKET s, charFAR*buf, int len, int flags);

        * 第一个参数指定接收端套接字描述符;
        * 第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
        * 第三个参数指明buf的长度;
        * 第四个参数一般置0。
        * recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
        * 同步Socket的recv函数的执行流程。当应用程序调用recv函数时:
        * (1)recv先**等待**s的发送缓冲中的数据被协议**传送完毕**,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,
        * (2)如果s的发送缓冲中**没有数据或者数据被协议成功发送完毕**后,recv先**检查**套接字s的接收缓冲区,如果s接收缓冲区中**没有数据或者协议正在接收数据**,那么recv就一直**等待**,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的)

        * int **send**( SOCKET s, const char FAR *buf, int len, int flags );

        * (1)第一个参数指定发送端套接字描述符;
        * (2)第二个参数指明一个存放应用程序要发送数据的缓冲区;
        * (3)第三个参数指明实际要发送的数据的字节数;
        * (4)第四个参数一般置0。
        * 同步Socket的send函数的执行流程。当调用该函数时:
        * send先比较待发送数据的长度len和套接字s的发送缓冲的长度, 如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR
        * 如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议是否正在发送s的发送缓冲中的数据
        * 如果是就**等待**协议把数据发送完,
        * 如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的**剩余空间**和len,如果len大于剩余空间大小send就一直**等待**协议把s的发送缓冲中的数据发送完,如果len小于剩余空间大小send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)
        * 如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
        * 注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。
        * 在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

        * **int** accept(**int** sockfd, struct sockaddr *addr, socklen_t *addrlen)

        * 大部分资料对于accept函数第三个参数的描述如下:连线成功时,参数addr所指的结构会被系统填入远程主机的地址数据,参数addrlen为scokaddr的结构长度。
        * 如果将addrlen指针所指向的值中的数据不初始化或初始化为一个小于sizeof(struct sockaddr)的值时,所获取的客户机地址就会出现错误。
        * 官方关于accept的*addrlen参数解释如下:这里的addrlen所指向的值,是必须初始化的,而且要初始化为一个大于等于实际获取socket的数据长度的值,而accept函数在执行后,会将实际值赋给addrlen所指向的值,故如果期望值小于实际值,所获取的数据在存储时就会发生溢出,读取时所得值便产生了错误。
        * 与connect函数不同的就在于addrlen是要被赋值的,因为addr要被赋值,因此要传入一个初始化了的指针的地址。而connect只需要告知addr的大小,因此传入一个值

        ### 简单代码实现

        ```c++
        //server
        #include <sys/socket.h>
        #include <netinet/in.h>//sockaddr_in
        #include <arpa/inet.h>//in_addr
        #include <string.h>
        #include <iostream>//cerr
        #include <unistd.h>//close
        #define myport 8000


        int main()
        {
        //定义socketfd,它要绑定监听的网卡地址和端口
        int listenfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字

        //定义sockaddr_in
        struct sockaddr_in socketaddr;
        socketaddr.sin_family = AF_INET;//ipv4
        socketaddr.sin_port = htons(myport);//字节序转换
        socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;
        //因为路由的关系,从客户端来的IP包只可能到达其中一个网卡。指定了网卡地址的话,必须从相应的地址进入才能连接到port
        //#define INADDR_ANY ((in_addr_t) 0x00000000)

        //绑定套接字和地址端口信息,sockaddr_in转成sockaddr
        if(bind(listenfd,(struct sockaddr *)&socketaddr,sizeof(socketaddr))==-1)
        {
        std::cerr<<"bind"<<std::endl;
        exit(1);
        //cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲.
        //也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。
        }
        std::cout<<"listen socket port: "<<myport<<std::endl;

        //开始监听
        if(listen(listenfd,SOMAXCONN) == -1)
        {
        std::cerr<<"listen"<<std::endl;
        exit(1);
        }

        ///客户端套接字
        char buffer[1024];
        struct sockaddr_in client_addr;//获取客户的地址和端口号,连接后的不分配新端口
        socklen_t len = sizeof(client_addr);//socklen_t 相当于 int,但使用int必须强制转型告知编译器
        std::cout<<"wating for conn..."<<std::endl;

        int conn = accept(listenfd, (struct sockaddr*)&client_addr, &len);//阻塞,等待连接,成功则创建连接套接字conn描述这个用户
        if(conn==-1)
        {
        std::cerr<<"connect"<<std::endl;
        exit(1);
        }
        /*
        如果队列中没有等待的连接,套接字也没有被标记为Non-blocking,accept()会阻塞调用函数直到连接出现;
        如果套接字被标记为Non-blocking,队列中也没有等待的连接,accept()返回错误EAGAIN或EWOULDBLOCK。
        */
        std::cout<<"conn successfully: port-"<<ntohs(client_addr.sin_port)<<" ip-"<<inet_ntoa(client_addr.sin_addr)<<std::endl<<std::endl;
        std::string str = "receive successfully";
        while(1)
        {
        bzero(buffer,sizeof(buffer));//每次都将buffer清空,防止被上次写入的结果影响,和memset(buffer,0,sizeof(buffer));等价
        int len = recv(conn, buffer, sizeof(buffer),0);//同步接收,是阻塞的
        //客户端发送exit或者异常结束时,退出
        if(strcmp(buffer,"exit\n")==0 || len<=0)
        {
        std::cout<<"break!"<<std::endl;
        break;
        }

        std::cout<<"receive: "<<buffer;//fgets本身不会去掉\n,这里不用endl
        send(conn, str.c_str(), str.size(), 0);//发回成功信息
        }

        close(conn);
        close(listenfd);
        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
50
51
52
53
54
55
//client
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <string.h>
#include <iostream>//cerr
#include <unistd.h>//close
#define myport 8000
const char* SERVER_IP = "127.0.0.1";//或"192.168.248.131"
int main()
{
//定义connect socket
int connfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(myport);//服务器端口,自己连接后的端口是os分配的,由进程选择一个端口去连服务器
//socketaddr.sin_addr.s_addr = inet_addr(SERVER_IP); ///服务器ip
//inet_addr最好换成inet_aton(),不会冤枉0.0.0.0和255.255.255.255
struct in_addr inaddr;
inet_aton(SERVER_IP,&inaddr);
socketaddr.sin_addr = inaddr;
std::cout<<"connect to "<<SERVER_IP<<" "<<myport<<std::endl;

///连接服务器,成功返回0,错误返回-1。返回的描述符connfd,该socket包含了服务器ip、port,自己ip、port,可用于发送和接收数据
if (connect(connfd, (struct sockaddr *)&socketaddr, sizeof(socketaddr)) == -1)//
{
std::cerr<<"connect error"<<std::endl;
exit(1);
}
std::cout<<"connect to server successfully"<<std::endl;
char sendbuf[1024];
char recvbuf[1024];
//gets:从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时停止
//如果成功,该函数返回 str。如果发生错误或者到达文件末尾时还未读取任何字符,则返回 NULL。
//等价于fgets(sendbuf, sizeof(sendbuf), stdin)

std::cout<<"send> ";
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)//gets已不被编译器支持,不太安全
{
std::cout<<"send to server: "<<sendbuf;//不用换行fgets包含了\n
send(connfd, sendbuf, strlen(sendbuf),0); ///发送
if(strcmp(sendbuf,"exit\n")==0)
break;
recv(connfd, recvbuf, sizeof(recvbuf),0); ///接收
std::cout<<"receive from server: "<<recvbuf<<std::endl;

bzero(sendbuf,sizeof(sendbuf));
bzero(recvbuf,sizeof(recvbuf));
std::cout<<"send> ";
}
close(connfd);
return 0;
}

编译就正常g++编译

image-20221008231532602

socket-Windows

基本的socket操作在<WinSock2.h>这个头文件,包括许多使用到的网络相关的结构体和函数。

  • 加载/释放Winsock库:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //初始化WSA
    WORD sockVersion=MAKEWORD(2,2);
    WSADATA wsaData;//WSADATA结构体变量的地址值
    /*
    MAKEWORD 语法如下:
    WORD MAKEWORD(
    BYTE below; //指定一个低位的新值
    BYTE high; //指定一个高位的新值
    );
    先将两个参数转换为二进制,将第一个参数放在低位,第二个参数放在高位,
    高位字节指明副版本、低位字节指明主版本,最后转换为十进制,赋给 sockVersion。

    这一步是为了声明调用不同的WinSock版本。例如MAKEWORD(2,2)就是调用2.2版本,MAKEWORD(1,1) 就是调用1.1版。
    不同版本是有区别的,例如1.1版只支持TCP/IP协议,而2.0版可以支持多协议。
    2.0版有良好的向后兼容性,任何使用1.1版的源代码、二进制文件、应用程序都可以不加修改地在2.0规范下使用。此外 WinSock 2.0 支持异步,1.1不支持异步。

    WSADATA 是一个结构体,用于存放 socket 的初始化信息。wsaData 用于存放结构体变量的地址值。
    */
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //初始化socket资源
    // 当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,
    // 然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了
    if(WSAStartup(sockVersion, &wsaData)!=0)
    {
    return 0; //代表失败
    }
    /*
    WSAStartup()的原型如下: int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
    如果WSA初始化成功,函数会返回0,失败时会返回非零的错误代码值。所以如果函数返回值不等于0,则打印错误信息,程序终止。

    */
    1
    2
    3
    4
    5
    6
    7
    8
    9
    WSACleanup();
    /*
    WSACleanup() 与开头的 WSAStartup() 函数是成对使用的,用于解除与 Socket 库的绑定并且释放 Socket 库所占用的系统资源。

    在 Windows 下,Socket 是以 DLL 的形式实现的。在 DLL 内部维持着一个计数器,
    只有第一次调用 WSAStartup 才真正装载DLL,以后的 调用只是简单的增加计数器,
    而WSACleanup 函数的功能则刚好相反,每调用一次使计数器减1,当计数器减到0时,DLL 就从内存中被卸载!
    因此,你调用了多少次 WSAStartup ,就应相应的调用多少次的WSACleanup。
    */
  • 创建socket使用SOCKET这一关键字;关闭socket使用closesocket()函数。

  • 没有bzero函数,使用memset

  • 没有inet_aton(),需要使用inet_pton(),头文件是<WS2tcpip.h>,多了第一个参数,AF_INET或AF_INET6,指定ipv4或ipv6。

  • INVALID_SOCKET值为int的-1

  • #pragma comment(lib,”ws2_32.lib”)表示链接 Ws2_32.lib 这个库,加载dll。

    • 这种方式和在工程设置_链接库里面添加 Ws2_32.lib 的效果一样,不过这种方法写的程序,别人在使用你的代码的时候就不用再设置工程了。使用 DLL 之前必须把 DLL 加载到当前程序,这里在程序运行时加载,表示加载了ws2_32.dll
  • 比较新的操作是:初始化WSA资源,从而根据设置在dll文件中找到对应的库;释放WSA资源,解除库占用。其他都和linux差不多。而这几步不同的操作基本都是复制粘贴不需要变的。

  • 关于lib和dll

1
2
3
4
5
6
7
8
9
10
lib库有两种:

1、静态链接库(Static Link Library)
这种 lib 中有函数的实现代码,它是将 lib 中的代码加入目标模块(.exe 或者 .dll)文件中,所以链接好了之后,lib 文件就没有用了。
这种 lib文件实际上是任意个 obj 文件的集合。obj 文件则是 cpp 文件编译生成的,
如果有多个 cpp 文件则会编译生成多个 obj 文件,从而生成的 lib 文件中也包含了多个 obj。

2、动态链接库(Dynamic Link Library)的导入库(Import Library)
这种 lib 是和 dll 配合使用的,里面没有代码,代码在 dll 中,这种 lib 是用在静态调用 dll 上的,所以起的作用也是链接作用,
链接完成了, lib 也没用了。至于动态调用 dll 的话,根本用不上 lib 文件。目标模块(exe 或者 dll)文件生成之后,就用不着 lib 文件了。

客户端实现

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
/*****************************************************************************************************************************
* 1、加载套接字库,创建套接字(WSAStartup()/socket());
* 2、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());
*****************************************************************************************************************************/

#include<iostream>
#include<cstring> //相当于string.h,使用strlen、memset函数。string头文件拥有string类,也可以使用string.h的函数,但是在std名称空间
#include<WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton

#pragma comment(lib,"ws2_32.lib")


#define myport 8000
const char* SERVER_IP = "192.168.248.131";
int main()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cout << "WSAStartup() error!" << std::endl;
return 0;
}

//创建套接字
SOCKET connfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (connfd == INVALID_SOCKET)
{
std::cout << "socket error !" << std::endl;
return 0;
}

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(myport);//服务器端口,自己连接后的端口是os分配的,由进程选择一个端口去连服务器
//socketaddr.sin_addr.s_addr = inet_addr(SERVER_IP); ///服务器ip
//inet_addr最好换成inet_aton(),不会冤枉0.0.0.0和255.255.255.255
struct in_addr inaddr;
inet_pton(AF_INET, SERVER_IP, &inaddr);//windows下相当于inet_aton的函数,多了第一个参数表明是ipv4还是ipv6
socketaddr.sin_addr = inaddr;
std::cout << "connect to " << SERVER_IP << " " << myport << std::endl;

///连接服务器,成功返回0,错误返回-1。返回的描述符connfd,该socket包含了服务器ip、port,自己ip、port,可用于发送和接收数据
if (connect(connfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)//
{
std::cerr << "connect error" << std::endl;
exit(1);
}
std::cout << "connect to server successfully" << std::endl;
char sendbuf[1024];
char recvbuf[1024];
//windows下初始化好像有些奇怪,这里先手动清0
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
//gets:从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时停止
//如果成功,该函数返回 str。如果发生错误或者到达文件末尾时还未读取任何字符,则返回 NULL。
//等价于fgets(sendbuf, sizeof(sendbuf), stdin)

std::cout << "send> ";
while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)//gets已不被编译器支持,不太安全
{
std::cout << "send to server: " << sendbuf;//不用换行fgets包含了\n
send(connfd, sendbuf, strlen(sendbuf), 0); ///发送
if (strcmp(sendbuf, "exit\n") == 0)
break;
recv(connfd, recvbuf, sizeof(recvbuf), 0); ///接收
std::cout << "receive from server: " << recvbuf << std::endl;

//windows下不支持bzero
//bzero(sendbuf, sizeof(sendbuf));
//bzero(recvbuf, sizeof(recvbuf));
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
std::cout << "send> ";
}
closesocket(connfd);
WSACleanup();
return 0;
}

image-20221009150801980

使用vs2022跑程序,可以跑通,虚拟机的网卡并没有设置其他的东西,使用了默认的NAT模式就行,不过有可能要配置端口转发,好在我的机器不用-。-

exe生成发布

参考(29条消息) 在VisualStudio上生成代码的exe可执行文件_hunk954的博客-CSDN博客_visualstudio怎么生成exe

其中关于多线程MT和多线程MD,可以参考多线程MT和多线程MD的区别 - 繁星jemini - 博客园 (cnblogs.com),使用MD较多,小项目就没啥差别了。

socket-文件传输

服务器接收文件,客户端发送文件

客户端:主要是打开文件,然后读到缓冲区,不断发送

  • FILE *fopen(const char *filename, const char *mode)

    • filename – 字符串,表示要打开的文件名称。
    • mode – 字符串,表示文件的访问模式,可以是以下的值:
      • r 以只读方式打开文件,该文件必须存在。
      • r+ 以可读写方式打开文件,该文件必须存在。
      • rb+ 读写打开一个二进制文件,允许读数据。
      • rw+ 读写打开一个文本文件,允许读和写。
      • w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
      • w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
      • a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
      • a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。 (原来的EOF符不保留)
      • wb 只写打开或新建一个二进制文件;只允许写数据。
      • wb+ 读写打开或建立一个二进制文件,允许读和写。
      • ab+ 读写打开一个二进制文件,允许读或在文件末追加数据。
  • vs已不支持fopen,使用fopen_s(linux下还是用fopen):errno_t fopen_s( FILE** pFile, const char *filename, const char *mode );

    • err = fopen_s(&fp,“filename”,“w”);
    • 打开文件成功返回0,失败返回非0。
  • int ferror(FILE *stream)

    • 测试给定流 stream 的错误标识符。如果出错返回一个非零值,否则返回0。
  • int feof(FILE *stream)

    • 测试给定流 stream 的文件结束标识符。如果fread后文件读完了(指针到文件末尾),返回非零值,否则返回0
  • size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)

    • ptr – 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。
    • size – 这是要读取的每个元素的大小,以字节为单位。
    • nmemb – 这是元素的个数,每个元素的大小为 size 字节。
    • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。
    • 成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾。
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
/*****************************************************************************************************************************
* 1、加载套接字库,创建套接字(WSAStartup()/socket());
* 2、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());
*****************************************************************************************************************************/

#include<iostream>
#include<cstring> //相当于string.h,使用strlen、memset函数。string头文件拥有string类,也可以使用string.h的函数,但是在std名称空间
#include<WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton
#include <stdio.h>//FILE等操作
#include <chrono>
#pragma comment(lib,"ws2_32.lib")


#define myport 8000
#define buffSize 102400
const char* SERVER_IP = "192.168.248.131";
int main()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cout << "WSAStartup() error!" << std::endl;
return 0;
}

//创建套接字
SOCKET connfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (connfd == INVALID_SOCKET)
{
std::cout << "socket error !" << std::endl;
return 0;
}

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(myport);//服务器端口,自己连接后的端口是os分配的,由进程选择一个端口去连服务器
//socketaddr.sin_addr.s_addr = inet_addr(SERVER_IP); ///服务器ip
//inet_addr最好换成inet_aton(),不会冤枉0.0.0.0和255.255.255.255
struct in_addr inaddr;
inet_pton(AF_INET, SERVER_IP, &inaddr);//windows下相当于inet_aton的函数,多了第一个参数表明是ipv4还是ipv6
socketaddr.sin_addr = inaddr;
std::cout << "connect to " << SERVER_IP << " " << myport << std::endl;

///连接服务器,成功返回0,错误返回-1。返回的描述符connfd,该socket包含了服务器ip、port,自己ip、port,可用于发送和接收数据
if (connect(connfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)//
{
std::cerr << "connect error" << std::endl;
exit(1);
}
std::cout << "connect to server successfully" << std::endl;



char sendbuf[buffSize];
//windows下初始化好像有些奇怪,这里先手动清0
memset(sendbuf, 0, sizeof(sendbuf));

//----------------------打开文件------------------------------------------------------
const char* filename = "test.pdf";
FILE* fp = NULL;
if (fopen_s(&fp,filename, "rb") != 0)//要以二进制形式读写,这样兼容文件格式
{
std::cerr << "cannot open file " << filename << std::endl;
exit(1);
}
std::chrono::system_clock::time_point time1 = std::chrono::system_clock::now();
int nSend = 0;
int totalSend = 0;
while (1)
{
int nRead = fread(sendbuf, 1, buffSize, fp);

if (ferror(fp) != 0)
{
std::cerr << "failed to read file " << std::endl;
exit(1);
}

nSend = send(connfd, sendbuf, nRead, 0);//发送nRead个字节

if (nSend == SOCKET_ERROR)//网络断开或copy出错
{
std::cerr << "the connection to server has been failed" << std::endl;
exit(1);
}
totalSend += nSend;
printf("success to send %d bytes\n", nSend);

if (feof(fp))//读了,发完,再判断是否到达末尾
{
printf("success to transmit file to server\n");
break;
}
}
std::chrono::system_clock::time_point time2 = std::chrono::system_clock::now();
printf("success to send %d bytes totally\n", totalSend);
std::cout<<"spend "<<std::chrono::duration_cast<std::chrono::milliseconds>(time2-time1).count()<<" ms"<<std::endl;
closesocket(connfd);
WSACleanup();
return 0;
}

服务器端:

  • size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

    • ptr – 这是指向要被写入的元素数组的指针。
    • size – 这是要被写入的每个元素的大小,以字节为单位。
    • nmemb – 这是元素的个数,每个元素的大小为 size 字节。
    • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。
    • 如果成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与 nmemb 参数不同,则会显示一个错误。
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
//server
#include <sys/socket.h>
#include <netinet/in.h>//sockaddr_in
#include <arpa/inet.h>//in_addr
#include <string.h>
#include <iostream>//cerr
#include <unistd.h>//close
#include <stdio.h>
#define myport 8000
#define buffSize 102400

int main()
{
//定义socketfd,它要绑定监听的网卡地址和端口
int listenfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(myport);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;
//因为路由的关系,从客户端来的IP包只可能到达其中一个网卡。指定了网卡地址的话,必须从相应的地址进入才能连接到port
//#define INADDR_ANY ((in_addr_t) 0x00000000)

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if(bind(listenfd,(struct sockaddr *)&socketaddr,sizeof(socketaddr))==-1)
{
std::cerr<<"bind"<<std::endl;
exit(1);
//cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲.
//也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。
}
std::cout<<"listen socket port: "<<myport<<std::endl;

//开始监听
if(listen(listenfd,SOMAXCONN) == -1)
{
std::cerr<<"listen"<<std::endl;
exit(1);
}

///客户端套接字
char recvbuf[buffSize];
struct sockaddr_in client_addr;//获取客户的地址和端口号,连接后的不分配新端口
socklen_t len = sizeof(client_addr);//socklen_t 相当于 int,但使用int必须强制转型告知编译器
std::cout<<"wating for conn..."<<std::endl;

int conn = accept(listenfd, (struct sockaddr*)&client_addr, &len);//阻塞,等待连接,成功则创建连接套接字conn描述这个用户
if(conn==-1)
{
std::cerr<<"connect"<<std::endl;
exit(1);
}
/*
如果队列中没有等待的连接,套接字也没有被标记为Non-blocking,accept()会阻塞调用函数直到连接出现;
如果套接字被标记为Non-blocking,队列中也没有等待的连接,accept()返回错误EAGAIN或EWOULDBLOCK。
*/
std::cout<<"conn successfully: port-"<<ntohs(client_addr.sin_port)<<" ip-"<<inet_ntoa(client_addr.sin_addr)<<std::endl<<std::endl;
std::string str = "receive successfully";

//----------------------打开文件------------------------------------------------------
const char* filename = "test.pdf";
FILE *fp=NULL;
if((fp=fopen(filename,"wb"))==NULL)//要以二进制形式读写,这样兼容文件格式
{
std::cerr << "cannot open file "<<filename<<std::endl;
exit(1);
}

while(1)
{
int nRecv = recv( conn, recvbuf, buffSize, 0);
if( nRecv == SO_ERROR )//copy出错,linux下为SO_ERROR
{
std::cerr << "connection to client has been failed"<<std::endl;
exit(1);
}
else if( nRecv == 0 )//这种情况是对端close了,此时返回0。可能是意外close,也可能是发送完毕了
{
printf( "client close, maybe the file transmit successfully\n" );
break;
}

int nWrite=fwrite(recvbuf,1,nRecv,fp);//写文件

if(nWrite!=nRecv || ferror(fp)!=0 )
{
std::cerr <<"failed to write file"<<std::endl;
exit(1);
}
}

close(conn);
close(listenfd);
return 0;
}

结果

使用102400的buffer大小(比10240快),发送10M的文件耗时100多ms

image-20221009230059734

栈空间默认只有1M大小,所以buffer不能太大。可以试一下用堆空间

开一个1M的buffer空间,结果和栈差不多。因为访问栈会比访问堆快,访问堆的一个具体单元,需要两次访问内存,第一次得取得指针,第二次才是真正得数据,而栈只需访问一次。另外,堆的内容被操作系统交换到外存的概率比栈大,栈一般是不会被交换出去的。

发一个300M的pdf需要5s。

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
//server-windows
//server
#include<iostream>
#include<cstring> //相当于string.h,使用strlen、memset函数。string头文件拥有string类,也可以使用string.h的函数,但是在std名称空间
#include<WinSock2.h>//除了inet_pton
#include <WS2tcpip.h>//inet_pton
#include <stdio.h>//FILE等操作
#include <chrono>
#pragma comment(lib,"ws2_32.lib")

#define myport 8000
#define buffSize 102400

int main()
{
//初始化WSA
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;//WSADATA结构体变量的地址值

//int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//成功时会返回0,失败时返回非零的错误代码值
if (WSAStartup(sockVersion, &wsaData) != 0)
{
std::cout << "WSAStartup() error!" << std::endl;
return 0;
}
//定义socketfd,它要绑定监听的网卡地址和端口
int listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//第三个参数写0也可以,这里表示创建tcp套接字

//定义sockaddr_in
struct sockaddr_in socketaddr;
socketaddr.sin_family = AF_INET;//ipv4
socketaddr.sin_port = htons(myport);//字节序转换
socketaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示监听所有网卡地址,0.0.0.0;
//因为路由的关系,从客户端来的IP包只可能到达其中一个网卡。指定了网卡地址的话,必须从相应的地址进入才能连接到port
//#define INADDR_ANY ((in_addr_t) 0x00000000)

//绑定套接字和地址端口信息,sockaddr_in转成sockaddr
if (bind(listenfd, (struct sockaddr*)&socketaddr, sizeof(socketaddr)) == -1)
{
std::cerr << "bind" << std::endl;
exit(1);
//cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲.
//也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。
}
std::cout << "listen socket port: " << myport << std::endl;

//开始监听
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cerr << "listen" << std::endl;
exit(1);
}

///客户端套接字
char recvbuf[buffSize];
struct sockaddr_in client_addr;//获取客户的地址和端口号,连接后的不分配新端口
socklen_t len = sizeof(client_addr);//socklen_t 相当于 int,但使用int必须强制转型告知编译器
std::cout << "wating for conn..." << std::endl;

int conn = accept(listenfd, (struct sockaddr*)&client_addr, &len);//阻塞,等待连接,成功则创建连接套接字conn描述这个用户
if (conn == -1)
{
std::cerr << "connect" << std::endl;
exit(1);
}
/*
如果队列中没有等待的连接,套接字也没有被标记为Non-blocking,accept()会阻塞调用函数直到连接出现;
如果套接字被标记为Non-blocking,队列中也没有等待的连接,accept()返回错误EAGAIN或EWOULDBLOCK。
*/
std::string str = "receive successfully";

//----------------------打开文件------------------------------------------------------
const char* filename = "test.zip";
FILE* fp = NULL;
if (fopen_s(&fp, filename, "wb") != 0)//要以二进制形式读写,这样兼容文件格式
{
std::cerr << "cannot open file " << filename << std::endl;
exit(1);
}

while (1)
{
int nRecv = recv(conn, recvbuf, buffSize, 0);
if (nRecv == SO_ERROR)//copy出错,linux下为SO_ERROR
{
std::cerr << "connection to client has been failed" << std::endl;
exit(1);
}
else if (nRecv == 0)//这种情况是对端close了,此时返回0。可能是意外close,也可能是发送完毕了
{
printf("client close, maybe the file transmit successfully\n");
break;
}

int nWrite = fwrite(recvbuf, 1, nRecv, fp);//写文件

if (nWrite != nRecv || ferror(fp) != 0)
{
std::cerr << "failed to write file" << std::endl;
exit(1);
}
}

closesocket(listenfd);
closesocket(conn);
WSACleanup();
return 0;

}

epoll

epoll是对于服务器端来说的,可以用前面的客户端来连接验证。我们来简单看看epoll的底层。

select和epoll的区别(面试常考)

  • 首先select是posix支持的,而epoll是linux特定的系统调用,因此,epoll的可移植性就没有select好,但是考虑到epoll和select一般用作服务器的比较多,而服务器中大多又是linux,所以这个可移植性的影响应该不会很大。
  • 其次,select可以监听的文件描述符有限,最大值为1024,而epoll可以监听的文件描述符则是系统对整个进程限制的最大文件描述符。
  • 接下来就要谈epoll和select的性能比较了,这个一般情况下应该是epoll表现好一些,否则linux也不会去特定实现epoll函数了,那么epoll为什么比select更高效呢?原因有很多,第一点,epoll通过每次有就绪事件时都将其插入到一个就绪队列中,使得epoll_wait的返回结果中只存储了已经就绪的事件,而select则返回了所有被监听的事件,事件是否就绪需要应用程序去检测,那么如果已被监听但未就绪的事件较多的话,对性能的影响就比较大了。第二点,每一次调用select获得就绪事件时都要将需要监听的事件重复传递给操作系统内核,而epoll对监听文件描述符的处理则和获得就绪事件的调用分开,这样获得就绪事件的调用epoll_wait就不需要重新传递需要监听的事件列表,这种重复的传递需要监听的事件也是性能低下的原因之一。除此之外,epoll的实现中使用了mmap调用使得内核空间和用户空间共享内存,从而避免了过多的内核和用户空间的切换引起的开销。
  • 然后就是epoll提供了两种工作模式,一种是水平触发模式,这种模式和select的触发方式是一样的,要只要文件描述符的缓冲区中有数据,就永远通知用户这个描述符是可读的,这种模式对block和noblock的描述符都支持,编程的难度也比较小;而另一种更高效且只有epoll提供的模式是边缘触发模式,只支持nonblock的文件描述符,他只有在文件描述符有新的监听事件发生的时候(例如有新的数据包到达)才会通知应用程序,在没有新的监听时间发生时,即使缓冲区有数据(即上一次没有读完,或者甚至没有读),epoll也不会继续通知应用程序,使用这种模式一般要求应用程序收到文件描述符读就绪通知时,要一直读数据直到收到EWOULDBLOCK/EAGAIN错误,使用边缘触发就必须要将缓冲区中的内容读完,否则有可能引起死等,尤其是当一个listen_fd需要监听到达连接的时候,如果多个连接同时到达,如果每次只是调用accept一次,就会导致多个连接在内核缓冲区中滞留,处理的办法是用while循环抱住accept,直到其出现EAGAIN。这种模式虽然容易出错,但是性能要比前面的模式更高效,因为只需要监听是否有事件发生,发生了就直接将描述符加入就绪队列即可。

select的缺点

select的缺点:

  1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024)
  2. 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
  3. (不是返回就绪数组)select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

拿select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

1
2
3
4
5
通俗版本可能是这样的:
应用程序拿着一张纸:内核哥,我想知道这张纸上面三个表格中画1的地方对应的文件描述符,有没有发生啥事件。
内核说:程序弟,稍等。。。,我给你把没有事件的地方画上0,有事件的地方保留1.
内核在纸上一顿涂改操作,把没事件发生的地方改成了0,有事件的地方保留1,然后把纸交给程序。
然后,程序拿着纸,看着有1的地方就知道这地方发生了事件,他要去处理了。

设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?

在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

epoll底层

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

1
2
3
4
5
6
7
8
9
10
11
struct eventpoll{//置于缓存中,很快
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
/*
* 当事件到来或结束时,会用到红黑树的插入删除
*/

每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

1
2
3
4
5
有的人误以为 epoll 高效的全部因为这棵红黑树,这就有点夸大红黑树的作用了。
其实红黑树的作用是仅仅是在管理大量连接的情况下,添加和删除 socket 非常的高效。
如果 epoll 管理的 socket 固定的话,在数据收发的事件管理过程中其实红黑树是没有起作用的。
内核在socket上收到数据包以后,可以直接找到 epitem(epoll item),并把它插入到就绪队列里,然后等用户进程把事件取走。
这个过程中,红黑树的作用并不会得到体现。

所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。也是通过这个回调关系(更具体来说是一个指针),根据socket能直接找到epitem(我们创建记录的fd只是为了能找到socket),epitem中又包含了fd供用户使用。那为什么又要红黑树呢?因为要管理这些事件,当事件要关闭时还要找得到事件来关闭。

在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:

1
2
3
4
5
6
7
struct epitem{//事件对应一个fd
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

epoll.jpg

img

api

  • int epoll_create(int size)

    • 创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数,size不起作用。(从Linux 2.6.8开始,max_size参数将被忽略,但必须大于零。)
    • 成功时,返回一个非负文件描述符。发生错误时,返回-1,并且将errno设置为指示错误
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

    • 该函数用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除

    • epfd:为epoll_creat的句柄

    • op:表示动作,用3个宏来表示:

      • EPOLL_CTL_ADD (注册新的fd到epfd),相当于把fd加到epfd这棵红黑树上
      • EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
      • EPOLL_CTL_DEL (从epfd删除一个fd);
    • fd:文件描述符

    • event:告诉内核需要监听的事件,结构如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      typedef union epoll_data {
      void *ptr;
      int fd;
      __uint32_t u32;
      __uint64_t u64;
      } epoll_data_t;

      struct epoll_event {
      __uint32_t events; /* Epoll events,是一串比特,设置类型时把类型或起来 */
      epoll_data_t data; /* User data variable */
      };
    • events描述事件类型,其中epoll事件类型有以下几种

      • EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
      • EPOLLOUT:表示对应的文件描述符可以写
      • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
      • EPOLLERR:表示对应的文件描述符发生错误
      • EPOLLHUP:表示对应的文件描述符被挂断;
      • EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
      • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
  • int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

    • 该函数用于等待所监控文件描述符上有事件的产生,返回就绪的文件描述符个数

      • events:用来存内核得到事件的集合,

      • maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,

      • timeout:是超时时间

        • -1:阻塞
        • 0:立即返回,非阻塞
        • >0:指定毫秒,没有事件触发会等待,但有事件触发就立即返回
      • 返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

触发模式:

  • LT水平触发模式

    • 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll还会再次向应用程序通知此事件,直到该事件被处理完毕。
  • ET边缘触发模式

    • 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。
    • 必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain
  • ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,故效率要比LT模式高。LT模式是epoll的默认工作模式

  • EPOLLONESHOT

    • 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
    • 我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件

代码实现

只看epoll部分,其他先不考虑。是很简单的接收响应,不涉及对写事件的响应。这里是同步的代码,主要还是了解一下代码流程。

框架

几乎所有的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
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i)
{
if(events[i].data.fd==listenfd) //有新的连接
{
connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
}
else if( events[i].events&EPOLLIN ) //接收到数据,读socket
{
n = read(sockfd, line, MAXLINE)) < 0 //读
ev.data.ptr = md; //md为自定义类型,添加数据
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
}
else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
{
struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
}
else
{
//其他的处理
}
}
}

LT模式

(29条消息) epoll 的LT与ET实例_five丶的博客-CSDN博客代码参考的博客,有修改

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
#include <unistd.h>
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <errno.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <signal.h>
#include <netdb.h>
#include <sys/types.h>

//设置地址可重用
static int setReuseAddr(int fd){
if(fd < 0) return -1;
int on = 1;
if (setsockopt( fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
return -1;
}
return 0;
}

int main(){

const int port = 8000;
const int MAX_EVENT = 20;//最多有20个就绪事件

//ev用来设置事件状态,event数组接收就绪事件
struct epoll_event ev, event[MAX_EVENT];

const char * const local_addr = "127.0.0.1";
struct sockaddr_in server_addr = { 0 };
//初始化socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);//0表示根据type默认
if (-1 == listenfd) {
std::cout << "create listenfd failed:" << strerror(errno) << std::endl;
return -1;
}

if (setReuseAddr(listenfd) < 0){
std::cout << "set reuse addr failed:" << strerror(errno) << std::endl;
close(listenfd);
return -1;
}

server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);

if (bind(listenfd, (const struct sockaddr *)&server_addr, sizeof (server_addr)) < 0) {
std::cout << "bind port failed:" << strerror(errno) << std::endl;
close(listenfd);
return -1;
}

if (listen(listenfd, SOMAXCONN) < 0) {
std::cout << "listen failed:" << strerror(errno) << std::endl;
close(listenfd);
return -1;
}

// 创建epoll实例
int epfd = epoll_create(5);
if (-1 == epfd) {
std::cout << "create epoll failed" << strerror(errno) << std::endl;
return -1;
}

//添加listenfd 到epoll事件,事件绑定读事件
ev.data.fd = listenfd;
ev.events = EPOLLIN /* 默认为水平触发。 */;

if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) < 0) {
std::cout << "epoll add failed:" << strerror(errno) << std::endl;
close(listenfd);
close(epfd);
return 0;
}

for( ; ; ){
int nfds;
//等待io事件
nfds = epoll_wait(epfd, event, MAX_EVENT, -1);//-1是阻塞
std:: cout << "epoll_wait return" << std::endl;

for(int i = 0; i < nfds; i++){
uint32_t events = event[i].events;
//处理epoll出错和对端关闭情况
if (events & EPOLLERR || events & EPOLLHUP) {
std::cout << "epoll has error" << strerror(errno) << std::endl;
close (event[i].data.fd);
continue;
}
else if (event[i].data.fd == listenfd){
//LT模式下,每次触发只处理一次
struct sockaddr in_addr = { 0 };
socklen_t in_addr_len = sizeof (in_addr);
int client_sock = accept(listenfd, nullptr, nullptr);//nullptr表示不保存客户端信息
if (client_sock < 0){
break;
}

std::cout << "accept client" << std::endl;

ev.events = EPOLLIN;//添加事件读
ev.data.fd = client_sock;
if (epoll_ctl( epfd, EPOLL_CTL_ADD, client_sock, &ev) < 0){
std::cout << client_sock <<" epoll_ctl falied: " << std::endl;
close(client_sock);
close(listenfd);
close(epfd);
return 0;
}
}
else{
ssize_t result_len;
//为了测试LT模式如何处理大量数据,将buf容量设置为10
char buf[10] = { 0 };
result_len = read(event[i].data.fd, buf, sizeof(buf) - 1);
//处理对端关闭情况
if(result_len == -1){
close(event[i].data.fd);
}
else if(!result_len){
continue;
}
std::cout << "receive message:" << buf << std::endl;
send(event[i].data.fd, "receive!", 9, 0);//发回成功信息
}
}
}
close(listenfd);
close(epfd);
}

LT模式下对端ctrl+c关闭会导致服务器epoll_wait不阻塞一直循环。原因是产生了一个事件一直得不到解决,可以用EPOLLONESHOT(亲测可用),不过要重新设置文件描述符。这里可以先不管,测试和参考博客一样,且可以连接多台客户端。

ET模式

  • ET模式下每次write或read需要循环write或read直到返回EAGAIN错误。以读操作为例,这是因为ET模式只在socket描述符状态发生变化时才触发事件,如果不一次把socket内核缓冲区的数据读完,会导致socket内核缓冲区中即使还有一部分数据,该socket的可读事件也不会被触发
  • 根据上面的讨论,若ET模式下使用阻塞IO,则程序一定会阻塞在最后一次write或read操作,因此说ET模式下一定要使用非阻塞IO
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
#include <unistd.h>
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <errno.h>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <signal.h>
#include <netdb.h>
#include <sys/types.h>

//设置文件描述符非阻塞
static int setNonblock (int fd) {
if(fd < 0) return -1;
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) {
return -1;
}

flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0) {
return -1;
}
return 0;
}

//设置地址可重用
static int setReuseAddr(int fd){
if(fd < 0) return -1;
int on = 1;
if (setsockopt( fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
return -1;
}
return 0;
}

int main(){

const int port = 8000;
const int MAX_EVENT = 20;

struct epoll_event ev, event[MAX_EVENT];

const char * const local_addr = "127.0.0.1";
struct sockaddr_in server_addr = { 0 };
//初始化socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listenfd) {
std::cout << "create listenfd failed:" << strerror(errno) << std::endl;
return -1;
}

if (setReuseAddr(listenfd) < 0){
std::cout << "set reuse addr failed:" << strerror(errno) << std::endl;
close(listenfd);
return -1;
}

server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);

if (bind(listenfd, (const struct sockaddr *)&server_addr, sizeof (server_addr)) < 0) {
std::cout << "bind port failed:" << strerror(errno) << std::endl;
close(listenfd);
return -1;
}

if (setNonblock(listenfd) < 0) {
std::cout << "set listenfd nonblock failed:" << strerror(errno) << std::endl;
close(listenfd);
return -1;
}

if (listen(listenfd, 200) < 0) {
std::cout << "listen failed:" << strerror(errno) << std::endl;
close(listenfd);
return -1;
}

// 创建epoll实例
int epfd = epoll_create(5);
if (1 == epfd) {
std::cout << "create epoll failed" << strerror(errno) << std::endl;
return -1;
}

//添加listenfd 到epoll事件
ev.data.fd = listenfd;
ev.events = EPOLLIN | EPOLLET /* 边缘触发选项。 */;

if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) < 0) {
std::cout << "epoll add failed:" << strerror(errno) << std::endl;
close(listenfd);
close(epfd);
return 0;
}

for( ; ; ){
int nfds;
//等待io事件
nfds = epoll_wait(epfd, event, MAX_EVENT, -1);
std:: cout << "epoll_wait return" << std::endl;

for(int i = 0; i < nfds; i++){
uint32_t events = event[i].events;
//处理epoll出错和对端关闭情况
if (events & EPOLLERR || events & EPOLLHUP) {
std::cout << "epoll has error" << strerror(errno) << std::endl;
close (event[i].data.fd);
continue;
} else if (event[i].data.fd == listenfd){
//ET模式下,需要一次把所有数据读完,使用循环读取
for ( ; ; ){
struct sockaddr in_addr = { 0 };
socklen_t in_addr_len = sizeof (in_addr);
int client_sock = accept(listenfd, nullptr, nullptr);
if (client_sock < 0){
break;
}

std::cout << "accept client" << std::endl;

if (setNonblock(client_sock) < 0){
std::cout << client_sock <<" set non_block falied: " << std::endl;
close(client_sock);
return 0;
}

ev.events = EPOLLIN | EPOLLET;
ev.data.fd = client_sock;
if (epoll_ctl( epfd, EPOLL_CTL_ADD, client_sock, &ev) < 0){
std::cout << client_sock <<" epoll_ctl falied: " << std::endl;
close(client_sock);
return 0;
}
}
} else{
int done = 0;
for ( ; ; ){
ssize_t result_len;
char buf[10] = { 0 };
result_len = read(event[i].data.fd, buf, sizeof(buf) - 1);

if(result_len == -1){
if(errno != EAGAIN && errno != EWOULDBLOCK){
done = 1;
epoll_ctl( epfd, EPOLL_CTL_DEL, event[i].data.fd, nullptr);
}
break;
}
else if(!result_len){
done = 1;
break;
}

std::cout << "receive message:" << buf << std::endl;
send(event[i].data.fd, "receive!", 9, 0);//发回成功信息
}
if(done){
std::cout << "close connection" << std::endl;
close(event[i].data.fd);
}
}
}
}
close(listenfd);
close(epfd);
}

测试结果和博客一样,如果把ET模式下代码改成不一次读完,那么冗余的数据会在下次客户端send后接收。

EPOLLONESHOT

客户端ctrl+c不会出现像LT模式下那样有某个事件然后一直处理不了不断循环,因为ET模式只触发一次相同事件或者说只在从非触发到触发两个状态转换的时候儿才触发。

因此EPOLLONESHOT在ET模式下,主要是为了防止多线程操作同一个socket:socket接收到数据交给一个线程处理数据,在数据没有处理完之前又有新数据达到触发了事件,另一个线程被激活获得该socket,从而产生多个线程操作同一socket,即使在ET模式下也有可能出现这种情况。采用EPOLLONETSHOT事件的文件描述符上的注册事件只触发一次,要想重新注册事件则需要调用epoll_ctl重置文件描述符上的事件,这样前面的socket就不会出现竞态。

前言

前几天follow完了tinywebsever的项目,分析了很多代码,最后也能跑起来。不过感觉整体的代码框架有些杂乱,代码也有冗余、不清晰的地方,比如互斥锁在c++11已经有专门的实现,不需要自己实现了。

作者也推荐了另一个c++11写的更简洁更优雅的项目实现:markparticle/WebServer: C++ Linux WebServer服务器 (github.com)

上一个项目最大的好处是作者专门写了一系列分析的文章,而这个项目没有教程也没什么注释,因此打算再写篇博客分析一下代码,写下注释,更重要的是把代码框架、逻辑理清楚,以及看看c++11实现的方便之处。


另外,在这里说一下size_t,很多c系的程序员对这个类型用的比较少,但这个项目里经常出现。

可以参考下:(24 封私信 / 80 条消息) size_t 这个类型的意义是什么? - 知乎 (zhihu.com)

主要还是为了可移植性,不同平台对于size_t的大小不同,64位系统是8字节,32位系统是4字节。为了方便移植,许多库函数的参数、返回值都是size_t。当换了个平台时,可以不改动代码而传入、接收更大或更小的值;并且系统不会使用更大的类型,从而加快速度。注意这些都是相对只用int、unsigned int、unsigned long作为类型对比的结果,用size_t有弹性。

但是,一个size_t类型的参数的用途却是用户定义的,比如可以把size_t就当int用,用来数组寻址等等,也可以用它来接收函数返回的参数然后作为一些长度,这些长度表示字节、还是两个字节都是用户决定的,它本身的值是多少就是多少。

一般用于作索引和表示单字节长度:

  • size_t传达了语义:您立即知道它表示一个以字节为单位的大小或一个索引,而不仅仅是另一个整数。
  • std::size_t是任何sizeof表达式的类型,并且保证能够表达C ++中任何对象(包括任何数组)的最大大小。通过扩展,它也保证对任何数组索引都足够大,因此它是数组上逐个索引循环的自然类型。

C++11可以将{}初始化器用于任何类型(可以使用等号,也可以不使用),这是一种通用的初始化语法。

在C++11中,集合(列表)的初始化已经成为C++的一个基本功能,被称为“初始化列表(initializer list)”

1
2
3
4
int a[] = { 1, 2, 3 };            //C++98支持,C++11支持
int b[]{2, 3, 4}; //C++98不支持,C++11支持
vector<int> c{ 1, 2, 3 }; //C++98不支持,C++11支持
map<int, float> d = {{ 1, 1.0f }, { 2, 2.0f }, { 3, 3.0f } };//C++98不支持,C++11支持

线程池

应用了很多新特性,比较难理解,要耐心一点。

右值引用可参考:C++11右值引用(一看即懂) (biancheng.net)

std::move()可参考:C++11 move()函数:将左值强制转换为右值 (biancheng.net)

std::forward()可参考:C++11完美转发及实现方法详解 (biancheng.net)

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
/*
* @Author : mark
* @Date : 2020-06-15
* @copyleft Apache 2.0
*/

#ifndef THREADPOOL_H
#define THREADPOOL_H

#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
#include <functional>
class ThreadPool {//线程创建后就开始运行,顶层只用往线程池插入任务即可
public:
explicit ThreadPool(size_t threadCount = 8): pool_(std::make_shared<Pool>()) {//初始化一个pool
assert(threadCount > 0);
for(size_t i = 0; i < threadCount; i++) {//创建count个线程,每个线程绑定工作函数并detach分离
//lambda匿名函数,按值捕获pool_,本身是个指针,指向同一个实例。没有参数,省略“()”
std::thread([pool = pool_] {//接下来是函数体
std::unique_lock<std::mutex> locker(pool->mtx);//灵活锁,因为要取一个元素。不放while可以避免重复定义
while(true) {
if(!pool->tasks.empty()) {//有元素就取,这里一定要先锁再判断
auto task = std::move(pool->tasks.front());//左值转右值,转移task内存的所有权,把function取出来,调用移动构造函数
pool->tasks.pop();
locker.unlock();//取完可以解锁了
task();//工作
locker.lock();//工作完因为是while,再锁,接下来再取元素。
}
else if(pool->isClosed) break;//结束线程
else pool->cond.wait(locker);//没有元素,解锁并等待唤醒
}
}).detach();//在创建线程后,实现线程从主线程(进程)分离,这使得线程能在工作完后自动回收资源
}
}

ThreadPool() = default;

ThreadPool(ThreadPool&&) = default;

~ThreadPool() {
if(static_cast<bool>(pool_)) {//强制转型,pool_有指向的话就是true,那么就准备让线程退出
{
std::lock_guard<std::mutex> locker(pool_->mtx);
pool_->isClosed = true;//退出标识
}
pool_->cond.notify_all();//唤醒所有线程,要把工作都做完才退出
}
}

template<class F>//以模板定义的&&既能接受左值也能接受右值,但注意,task作为参数,有名且能寻址,成为了左值
void AddTask(F&& task) {//添加一个task,右值传入,使得传入的对象的所有权被task获取,
{
std::lock_guard<std::mutex> locker(pool_->mtx);
//c++11新特性,emplace使对象在内存中调用构造函数,push会先构造再拷贝构造
pool_->tasks.emplace(std::forward<F>(task));//完美转发,保留传入的左右值属性,
//直接传task是个左值,如果F是一个function<>类型,这会导致移动构造和拷贝构造的区别。如果F是一个普通函数或指针等,一律调用普通构造函数
}
pool_->cond.notify_one();//唤醒等待队列中的第一个线程
}

private:
struct Pool {//线程池结构体,相当于在类里再封装一层
std::mutex mtx;
std::condition_variable cond;
bool isClosed;
std::queue<std::function<void()>> tasks;//任务队列,元素是一个函数,执行任务,没有返回值。没有参数是因为bind绑定好了参数,不需要外部传
};
std::shared_ptr<Pool> pool_;//使用共享指针,能自动销毁pool实例
};


#endif //THREADPOOL_H


先介绍下std::mutex:头文件<mutex> ,实际上跟linux中pthread的互斥锁差不多,手动上锁和解锁。

  • lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。(2). 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
  • unlock(), 解锁,释放对互斥量的所有权。
  • try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况,(1). 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。(2). 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。(3). 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

真正好用的是std::lock_guard:头文件<mutex> ,使用RAII机制,退出作用域就解锁。

  • template <class _Mutex>
    class lock_guard { // class with destructor that unlocks a mutex
    public:
        using mutex_type = _Mutex;
        //无adopt_lock参数,构造时加锁
        explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
            _MyMutex.lock();
        }
        //有adopt_lock参数,构造时不加锁
        lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock
        //析构解锁
        ~lock_guard() noexcept {
            _MyMutex.unlock();
        }
        //屏蔽拷贝构造
        lock_guard(const lock_guard&) = delete; 
        lock_guard& operator=(const lock_guard&) = delete; 
    
    private:
        _Mutex& _MyMutex;
    };
    
  • lock_guard具有两种构造方法:

    1. lock_guard(mutex& m)
    2. lock_guard(mutex& m, adopt_lock)其中mutex& m是互斥量,参数adopt_lock表示假定调用线程已经获得互斥体所有权并对其进行管理了。

再说下std::unique_lock:头文件<mutex>,也是使用RAII机制,定义和lock_guard相同。

主要还是说下二者的对比:

  • std::unique_lock 与std::lock_guard都能实现自动加锁与解锁功能,但是std::unique_lock要比std::lock_guard更灵活,但是更灵活的代价是占用空间相对更大一点且相对更慢一点。
  • 它提供了lock()unlock()接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁。而lock_guard一锁就锁住一个作用域,直到退出才解锁,没有lock和unlock接口,有时只想锁住一段代码,用unique_lock就更灵活。
  • unique_locklock_guard都不能复制,lock_guard不能移动,但是unique_lock可以
  • 可以参考[c++11]多线程编程(五)——unique_lock - 简书 (jianshu.com)

条件变量std::condition_variable:头文件 <condition_variable>,和linux的差不多了,可以看下(29条消息) C++11多线程条件变量std::condition_variable详解(转 )_山城盛夏的博客-CSDN博客_std::condition_variable 详解,当然不看也可以,无非是等待和唤醒。


关于std::function,主要是用来包装函数的,像函数一样调用,具体可以参考之前的博客:lambda表达式 | JySama

std::function是一个函数包装器,该函数包装器模板能包装任何类型的可调用实体,如普通函数,函数对象,lamda表达式等。包装器可拷贝,移动等,并且包装器类型仅仅依赖于调用特征,而不依赖于可调用元素自身的类型。std::function是C++11的新特性,包含在头文件<functional>中。

一个std::function类型对象实例可以包装下列这几种可调用实体:函数、函数指针、成员函数、静态函数、lamda表达式和函数对象。std::function对象实例可被拷贝和移动,并且可以使用指定的调用特征来直接调用目标元素。当std::function对象实例未包含任何实际可调用实体时,调用该std::function对象实例将抛出std::bad_function_call异常。


std::forward():完美转发

当我们将一个右值引用传入函数时,他在实参中有了命名,所以继续往下传或者调用其他函数时,根据C++ 标准的定义,这个参数变成了一个左值。那么他永远不会调用接下来函数的右值版本,这可能在一些情况下造成拷贝。为了解决这个问题 C++ 11引入了完美转发,根据右值判断的推倒,调用forward 传出的值,若原来是一个右值,那么他转出来就是一个右值,否则为一个左值。这样的处理就完美的转发了原有参数的左右值属性,不会造成一些不必要的拷贝。

std::forward必须配合T&&来使用。例如T&&接受左值int&时,T会被推断为int&,而T&&接受右值int&&时,T被推断为int。


std::thread:头文件<thread>,可移动不可复制

  • 默认构造函数,创建一个空的 std::thread 执行对象: thread() noexcept;
  • 初始化构造函数,创建一个 std::thread 对象,该 std::thread 对象可被 joinable,新产生的线程会调用 fn 函数,该函数的参数由 args 给出。
    • template <class Fn, class… Args> explicit thread(Fn&& fn, Args&&… args);

数据库

数据库如出一辙,很好理解

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
/*
* @Author : mark
* @Date : 2020-06-16
* @copyleft Apache 2.0
*/
#ifndef SQLCONNPOOL_H
#define SQLCONNPOOL_H

#include <mysql/mysql.h>
#include <string>
#include <queue>
#include <mutex>
#include <semaphore.h>
#include <thread>
#include "../log/log.h"

class SqlConnPool {//创建全局唯一的数据库连接池,维护多个与数据库的连接
public:
static SqlConnPool *Instance();//单例,静态成员函数

MYSQL *GetConn();
void FreeConn(MYSQL * conn);
int GetFreeConnCount();

void Init(const char* host, int port,
const char* user,const char* pwd,
const char* dbName, int connSize);
void ClosePool();

private:
SqlConnPool();
~SqlConnPool();

int MAX_CONN_;
int useCount_;
int freeCount_;

std::queue<MYSQL *> connQue_;
std::mutex mtx_;
sem_t semId_;
};


#endif // SQLCONNPOOL_H

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
/*
* @Author : mark
* @Date : 2020-06-17
* @copyleft Apache 2.0
*/

#include "sqlconnpool.h"
using namespace std;

SqlConnPool::SqlConnPool() {//放init也行,或者init放这
useCount_ = 0;
freeCount_ = 0;
}

SqlConnPool* SqlConnPool::Instance() {//单例模式
static SqlConnPool connPool;
return &connPool;
}
//初始化连接池
void SqlConnPool::Init(const char* host, int port,
const char* user,const char* pwd, const char* dbName,
int connSize = 10) {
assert(connSize > 0);//条件判断
for (int i = 0; i < connSize; i++) {
MYSQL *sql = nullptr;//定义一个sql指针
sql = mysql_init(sql);//用这个指针初始化一个sql结构体,返回一个指向这个结构体的指针
if (!sql) {//错误判断,写日志
LOG_ERROR("MySql init error!");
assert(sql);//报错
}
sql = mysql_real_connect(sql, host,
user, pwd,
dbName, port, nullptr, 0);//init后就connect,连接数据库,返回一个可用连接
if (!sql) {
LOG_ERROR("MySql Connect error!");
}
connQue_.push(sql);//放入队列中
}
MAX_CONN_ = connSize;
sem_init(&semId_, 0, MAX_CONN_);//初始化信号量的值,这个0表示只能在当前进程的所有线程之间共享
}
//获取一个可用连接
MYSQL* SqlConnPool::GetConn() {
MYSQL *sql = nullptr;//句柄
if(connQue_.empty()){
LOG_WARN("SqlConnPool busy!");
return nullptr;
}
//为什么前面判断了空这里还要用信号量呢?原因是线程可能在队列非空时纷涌而至,但实际上没有那么多连接可用,因此还是要信号量阻塞buffer
sem_wait(&semId_);
{//lock的作用域
lock_guard<mutex> locker(mtx_);//如果能取,要互斥
sql = connQue_.front();
connQue_.pop();
}
return sql;
}
//释放一个连接,放回队列中
void SqlConnPool::FreeConn(MYSQL* sql) {
assert(sql);//判空,不能放回虚假的连接
lock_guard<mutex> locker(mtx_);//互斥放回
connQue_.push(sql);
sem_post(&semId_);//post
}
//关闭连接池
void SqlConnPool::ClosePool() {
lock_guard<mutex> locker(mtx_);//锁住先,避免在close时被使用
while(!connQue_.empty()) {//循环取
auto item = connQue_.front();//auto真给力...
connQue_.pop();
mysql_close(item);//关闭连接
}
//避免在使用库完成应用程序后发生内存泄漏(例如,在关闭与服务器的连接之后),
//可以显式调用mysql_library_end()。这样可以执行内存 Management 以清理和释放库使用的资源。
mysql_library_end();
}
//获取当前可用连接大小
int SqlConnPool::GetFreeConnCount() {
lock_guard<mutex> locker(mtx_);
return connQue_.size();
}
//析构
SqlConnPool::~SqlConnPool() {
ClosePool();
}

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
/*
* @Author : mark
* @Date : 2020-06-19
* @copyleft Apache 2.0
*/

#ifndef SQLCONNRAII_H
#define SQLCONNRAII_H
#include "sqlconnpool.h"

/* 资源在对象构造初始化 资源在对象析构时释放*/
class SqlConnRAII {//以参数形式获取一个数据库连接
public:
SqlConnRAII(MYSQL** sql, SqlConnPool *connpool) {//双指针修改sql,为了获取连接,传入connpool
assert(connpool);
*sql = connpool->GetConn();//获取连接
sql_ = *sql;//记录
connpool_ = connpool;//为了释放,需要记录sql和connpool
}

~SqlConnRAII() {
if(sql_) { connpool_->FreeConn(sql_); }//析构释放
}

private:
MYSQL *sql_;
SqlConnPool* connpool_;
};

#endif //SQLCONNRAII_H

日志系统

阻塞队列,用互斥锁再封装

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
/*
* @Author : mark
* @Date : 2020-06-16
* @copyleft Apache 2.0
*/
#ifndef BLOCKQUEUE_H
#define BLOCKQUEUE_H

#include <mutex>
#include <deque>
#include <condition_variable>
#include <sys/time.h>

template<class T>
class BlockDeque {
public:
explicit BlockDeque(size_t MaxCapacity = 1000);

~BlockDeque();

void clear();

bool empty();

bool full();

void Close();

size_t size();

size_t capacity();

T front();

T back();

void push_back(const T &item);

void push_front(const T &item);

bool pop(T &item);

bool pop(T &item, int timeout);

void flush();

private:
std::deque<T> deq_;

size_t capacity_;

std::mutex mtx_;

bool isClose_;

std::condition_variable condConsumer_;

std::condition_variable condProducer_;
};


template<class T>
BlockDeque<T>::BlockDeque(size_t MaxCapacity) :capacity_(MaxCapacity) {
assert(MaxCapacity > 0);
isClose_ = false;
}

template<class T>
BlockDeque<T>::~BlockDeque() {
Close();
};

template<class T>
void BlockDeque<T>::Close() {
{
std::lock_guard<std::mutex> locker(mtx_);
deq_.clear();//清除所有元素
isClose_ = true;
}
condProducer_.notify_all();//唤醒所有生产者,准备退出
condConsumer_.notify_all();//唤醒所有消费者,准备退出
};

template<class T>
void BlockDeque<T>::flush() {
condConsumer_.notify_one();//刷新,唤醒一个线程,准备工作
};

template<class T>
void BlockDeque<T>::clear() {
std::lock_guard<std::mutex> locker(mtx_);
deq_.clear();
}

template<class T>
T BlockDeque<T>::front() {
std::lock_guard<std::mutex> locker(mtx_);
return deq_.front();
}

template<class T>
T BlockDeque<T>::back() {
std::lock_guard<std::mutex> locker(mtx_);
return deq_.back();
}

template<class T>
size_t BlockDeque<T>::size() {
std::lock_guard<std::mutex> locker(mtx_);
return deq_.size();
}

template<class T>
size_t BlockDeque<T>::capacity() {
std::lock_guard<std::mutex> locker(mtx_);
return capacity_;
}
//有问题,前面close唤醒了线程,这里没有根据isclose变量直接退出,在pop那是退出了的,这里可能会卡住,一旦wait的太多,就可能一直写然后一直while。
//除非线程数严格小于容量
template<class T>
void BlockDeque<T>::push_back(const T &item) {
std::unique_lock<std::mutex> locker(mtx_);
while(deq_.size() >= capacity_) {//条件变量的等待方式
condProducer_.wait(locker);//阻塞,等待唤醒,但唤醒后还是需要while看条件,因为有多个生产者在竞争
}
deq_.push_back(item);
condConsumer_.notify_one();//唤醒一个消费者线程
}

template<class T>
void BlockDeque<T>::push_front(const T &item) {
std::unique_lock<std::mutex> locker(mtx_);
while(deq_.size() >= capacity_) {
condProducer_.wait(locker);
}
deq_.push_front(item);
condConsumer_.notify_one();
}

template<class T>
bool BlockDeque<T>::empty() {
std::lock_guard<std::mutex> locker(mtx_);
return deq_.empty();
}

template<class T>
bool BlockDeque<T>::full(){
std::lock_guard<std::mutex> locker(mtx_);
return deq_.size() >= capacity_;
}

template<class T>
bool BlockDeque<T>::pop(T &item) {
std::unique_lock<std::mutex> locker(mtx_);
while(deq_.empty()){
condConsumer_.wait(locker);
if(isClose_){//如果close了就return了
return false;
}
}
item = deq_.front();
deq_.pop_front();
condProducer_.notify_one();//唤醒一个生产者线程
return true;
}

template<class T>
bool BlockDeque<T>::pop(T &item, int timeout) {//增加了超时处理,push没有超时处理
std::unique_lock<std::mutex> locker(mtx_);
while(deq_.empty()){
if(condConsumer_.wait_for(locker, std::chrono::seconds(timeout))
== std::cv_status::timeout){
return false;//阻塞超时就结束
}
if(isClose_){//关了直接返回
return false;
}
}
item = deq_.front();
deq_.pop_front();
condProducer_.notify_one();
return true;
}

#endif // BLOCKQUEUE_H

std::chrono::seconds:一个类,获取多少时间,这里以临时变量的形式传给wait_for,持续…seconds,超时结果就是timeout,和cv_status的timeout相等,借此判断是否超时。

std::cv_status:定义于头文件 <condition_variable>,带作用域枚举 std::cv_status 描述定时等待是否因时限返回。成员:

  • no_timeout:条件变量因 notify_allnotify_one 或虚假地被唤醒
  • timeout:条件变量因时限耗尽被唤醒

wait_for:

返回值:若经过 rel_time 所指定的关联时限则为 std::cv_status::timeout,否则为 std::cv_status::no_timeout 。

1
2
std::cv_status wait_for( std::unique_lock<std::mutex>& lock,
const std::chrono::duration<Rep, Period>& rel_time);
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
/*
* @Author : mark
* @Date : 2020-06-16
* @copyleft Apache 2.0
*/
#ifndef LOG_H
#define LOG_H

#include <mutex>
#include <string>
#include <thread>
#include <sys/time.h>
#include <string.h>
#include <stdarg.h> // vastart va_end
#include <assert.h>
#include <sys/stat.h> //mkdir
#include "blockqueue.h"
#include "../buffer/buffer.h"

class Log {
public:
void init(int level, const char* path = "./log",
const char* suffix =".log",
int maxQueueCapacity = 1024);

static Log* Instance();//单例
static void FlushLogThread();//异步线程的回调函数,需要是staic,没有this隐藏参数

void write(int level, const char *format,...);
void flush();

int GetLevel();
void SetLevel(int level);
bool IsOpen() { return isOpen_; }

private:
Log();
void AppendLogLevelTitle_(int level);
virtual ~Log();
void AsyncWrite_();//互斥写

private:
static const int LOG_PATH_LEN = 256;
static const int LOG_NAME_LEN = 256;
static const int MAX_LINES = 50000;

const char* path_;
const char* suffix_;

int MAX_LINES_;

int lineCount_;
int toDay_;

bool isOpen_;

Buffer buff_;//一个日志仅有一个buffer,因为write被互斥锁锁住了
int level_;
bool isAsync_;

FILE* fp_;
std::unique_ptr<BlockDeque<std::string>> deque_; //智能指针,还没有实例
std::unique_ptr<std::thread> writeThread_;//指向一个thread,还没有实例
std::mutex mtx_;
};
//以宏的形式,感觉写个string形式也行
#define LOG_BASE(level, format, ...) \
do {\
Log* log = Log::Instance();\
if (log->IsOpen() && log->GetLevel() <= level) {\
log->write(level, format, ##__VA_ARGS__); \
log->flush();\
}\
} while(0);

#define LOG_DEBUG(format, ...) do {LOG_BASE(0, format, ##__VA_ARGS__)} while(0);
#define LOG_INFO(format, ...) do {LOG_BASE(1, format, ##__VA_ARGS__)} while(0);
#define LOG_WARN(format, ...) do {LOG_BASE(2, format, ##__VA_ARGS__)} while(0);
#define LOG_ERROR(format, ...) do {LOG_BASE(3, format, ##__VA_ARGS__)} while(0);

#endif //LOG_H

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
/*
* @Author : mark
* @Date : 2020-06-16
* @copyleft Apache 2.0
*/
#include "log.h"

using namespace std;

Log::Log() {
lineCount_ = 0;
isAsync_ = false;
writeThread_ = nullptr;
deque_ = nullptr;
toDay_ = 0;
fp_ = nullptr;
}

Log::~Log() {
if(writeThread_ && writeThread_->joinable()) {//如果写线程存在且需要join,就需要join
while(!deque_->empty()) {//不断唤醒消费者线程,把日志写完
deque_->flush();
};
deque_->Close();//关掉
writeThread_->join();//join
}
if(fp_) {//如果文件打开了
lock_guard<mutex> locker(mtx_);
flush();//刷新fp的缓冲区,还有唤醒线程的功能,这里没用
fclose(fp_);//关掉
}
}

int Log::GetLevel() {//日志系统级别,越高级能写的类型越多,日志系统没到对应级别不能写
lock_guard<mutex> locker(mtx_);
return level_;
}

void Log::SetLevel(int level) {
lock_guard<mutex> locker(mtx_);
level_ = level;
}

void Log::init(int level = 1, const char* path, const char* suffix,
int maxQueueSize) {
isOpen_ = true;
level_ = level;
if(maxQueueSize > 0) {//如果设置了阻塞队列大小,就是异步
isAsync_ = true;
if(!deque_) {
unique_ptr<BlockDeque<std::string>> newDeque(new BlockDeque<std::string>);//创建一个实例
deque_ = move(newDeque);//unique指针只能移动构造

std::unique_ptr<std::thread> NewThread(new thread(FlushLogThread));//异步同时要实例化一个写线程
writeThread_ = move(NewThread);//因为是单独线程,所以用unique指针,转移所有权用move
}
} else {
isAsync_ = false;
}

lineCount_ = 0;

time_t timer = time(nullptr);
struct tm *sysTime = localtime(&timer);
struct tm t = *sysTime;
path_ = path;
suffix_ = suffix;
char fileName[LOG_NAME_LEN] = {0};
snprintf(fileName, LOG_NAME_LEN - 1, "%s/%04d_%02d_%02d%s",
path_, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, suffix_);//日志文件名写到名称缓冲区
toDay_ = t.tm_mday;

{//互斥锁作用域,感觉没必要,init就主线程调用
lock_guard<mutex> locker(mtx_);
buff_.RetrieveAll();
if(fp_) { //如果文本打开了,就关了重新开
flush();
fclose(fp_);
}

fp_ = fopen(fileName, "a");//根据名称创建or打开
if(fp_ == nullptr) {//打开失败,没有目标文件夹,要先创建
mkdir(path_, 0777);//0777是最大的访问权
fp_ = fopen(fileName, "a");
}
assert(fp_ != nullptr);
}
}

void Log::write(int level, const char *format, ...) {
//获取时间
struct timeval now = {0, 0};
gettimeofday(&now, nullptr);
time_t tSec = now.tv_sec;
struct tm *sysTime = localtime(&tSec);
struct tm t = *sysTime;
//宏参数初始化
va_list vaList;

/* 日志日期 日志行数 */
if (toDay_ != t.tm_mday || (lineCount_ && (lineCount_ % MAX_LINES == 0)))//如果日志行数太多写满了,或着换了一天,要重新创建一个文件
{
unique_lock<mutex> locker(mtx_);//这个锁感觉放if外好一点,因为让一个线程进来创建好新文件更新day喝line就可以了,这样就会很多线程一起进这个if
locker.unlock();//等下要用一个锁,先创建好,解锁。这对于同步写有用,因为同步的话有很多线程会调用
//把lock锁if外,然后处理完,更新day和line,打开新文件,再解锁,其他线程就不会进if了

char newFile[LOG_NAME_LEN];
char tail[36] = {0};
snprintf(tail, 36, "%04d_%02d_%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);

if (toDay_ != t.tm_mday)//如果是换了一天
{//-72是什么鬼,不需要那么长吗?
snprintf(newFile, LOG_NAME_LEN - 72, "%s/%s%s", path_, tail, suffix_);
toDay_ = t.tm_mday;//给天赋值
lineCount_ = 0;
}
else {
snprintf(newFile, LOG_NAME_LEN - 72, "%s/%s-%d%s", path_, tail, (lineCount_ / MAX_LINES), suffix_);
}

locker.lock();
flush();
fclose(fp_);
fp_ = fopen(newFile, "a");//重新打开
assert(fp_ != nullptr);
}
//然后正常写
{
unique_lock<mutex> locker(mtx_);
lineCount_++;//写一行
//buffer不只是为日志系统写的,而且感觉这里不用buffer更好
int n = snprintf(buff_.BeginWrite(), 128, "%d-%02d-%02d %02d:%02d:%02d.%06ld ",
t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
t.tm_hour, t.tm_min, t.tm_sec, now.tv_usec);//向buffer写时间信息,n是写入的长度

buff_.HasWritten(n);//移动指针,前n个写时间信息
AppendLogLevelTitle_(level);//然后添加等级

va_start(vaList, format);//遍历参数
int m = vsnprintf(buff_.BeginWrite(), buff_.WritableBytes(), format, vaList);//不断向buffer写,返回写入长度
va_end(vaList);//关闭

buff_.HasWritten(m);//移动指针
buff_.Append("\n\0", 2);//加换行和终止

if(isAsync_ && deque_ && !deque_->full()) {//如果是异步的,放阻塞队列
deque_->push_back(buff_.RetrieveAllToStr());
}
else {//如果是同步的,开写
fputs(buff_.Peek(), fp_);
}
buff_.RetrieveAll();//清空缓冲区
}
}

void Log::AppendLogLevelTitle_(int level) {//添加信息头
switch(level) {
case 0:
buff_.Append("[debug]: ", 9);
break;
case 1:
buff_.Append("[info] : ", 9);
break;
case 2:
buff_.Append("[warn] : ", 9);
break;
case 3:
buff_.Append("[error]: ", 9);
break;
default:
buff_.Append("[info] : ", 9);
break;
}
}

void Log::flush() {//刷新缓冲区
if(isAsync_) {
deque_->flush();
}
fflush(fp_);//刷新文本的缓冲区,强制写完
}

void Log::AsyncWrite_() {//回调函数的运行函数
string str = "";
//这里只用了非超时的,如果有大量任务突然到来,可以创建多个线程使用超时pop
while(deque_->pop(str)) {//不断取str,写进文本
lock_guard<mutex> locker(mtx_);
fputs(str.c_str(), fp_);
}
}

Log* Log::Instance() {//单例函数
static Log inst;
return &inst;
}

void Log::FlushLogThread() {//回调函数,调用运行函数
Log::Instance()->AsyncWrite_();
}

缓冲区

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
/*
* @Author : mark
* @Date : 2020-06-26
* @copyleft Apache 2.0
*/

#ifndef BUFFER_H
#define BUFFER_H
#include <cstring> //perror
#include <iostream>
#include <unistd.h> // write
#include <sys/uio.h> //readv
#include <vector> //readv
#include <atomic>
#include <assert.h>
class Buffer {
public:
Buffer(int initBuffSize = 1024);
~Buffer() = default;

size_t WritableBytes() const;
size_t ReadableBytes() const ;
size_t PrependableBytes() const;

const char* Peek() const;
void EnsureWriteable(size_t len);
void HasWritten(size_t len);

void Retrieve(size_t len);
void RetrieveUntil(const char* end);

void RetrieveAll() ;
std::string RetrieveAllToStr();

const char* BeginWriteConst() const;
char* BeginWrite();

void Append(const std::string& str);
void Append(const char* str, size_t len);
void Append(const void* data, size_t len);
void Append(const Buffer& buff);

ssize_t ReadFd(int fd, int* Errno);
ssize_t WriteFd(int fd, int* Errno);

private:
char* BeginPtr_();
const char* BeginPtr_() const;
void MakeSpace_(size_t len);

std::vector<char> buffer_;//buffer是一个vector...new一个得了,取地址比较简明
std::atomic<std::size_t> readPos_;//原子类型,感觉还是用个互斥锁吧...资料太少了,查不到。不过操作buffer在顶层是被互斥锁锁住的,也许不用互斥
std::atomic<std::size_t> writePos_;
};

#endif //BUFFER_H

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
/*
* @Author : mark
* @Date : 2020-06-26
* @copyleft Apache 2.0
*/
#include "buffer.h"

Buffer::Buffer(int initBuffSize) : buffer_(initBuffSize), readPos_(0), writePos_(0) {}//初始化列表,使用构造函数

size_t Buffer::ReadableBytes() const {//准备好的字节数
return writePos_ - readPos_;//原子地相减
}
size_t Buffer::WritableBytes() const {//可写入的字节数
return buffer_.size() - writePos_;
}

size_t Buffer::PrependableBytes() const {//已读字节数
return readPos_;
}

const char* Buffer::Peek() const {//返回readpos之后的字符串/位置,即准备好但没有取出的数据的起始地址
return BeginPtr_() + readPos_;//定位
}

void Buffer::Retrieve(size_t len) {//buffer被取出了多少字节
assert(len <= ReadableBytes());
readPos_ += len;
}

void Buffer::RetrieveUntil(const char* end) {//过滤到end之后
assert(Peek() <= end );//已读的记录比已取出的小,地址的比较
Retrieve(end - Peek());//被出去了这么多字节(地址)
}

void Buffer::RetrieveAll() {//全部取出,清空
bzero(&buffer_[0], buffer_.size());
readPos_ = 0;
writePos_ = 0;
}

std::string Buffer::RetrieveAllToStr() {//全部取出并转字string
std::string str(Peek(), ReadableBytes());
RetrieveAll();
return str;
}

const char* Buffer::BeginWriteConst() const {//返回写到的位置之后的字符串,有何意义?后面不是没写到吗
return BeginPtr_() + writePos_;
}

char* Buffer::BeginWrite() {//指向第一个能写的位置
return BeginPtr_() + writePos_;
}

void Buffer::HasWritten(size_t len) {//已写入多少个字节
writePos_ += len;
}

void Buffer::Append(const std::string& str) {//重载函数
Append(str.data(), str.length());
}

void Buffer::Append(const void* data, size_t len) {//任何指针类型data
assert(data);
Append(static_cast<const char*>(data), len);//其他类型,就强制转型为char*
}
//最终调用这个函数
void Buffer::Append(const char* str, size_t len) {//char*优先匹配
assert(str);
EnsureWriteable(len);//确保空间足够
std::copy(str, str + len, BeginWrite());
HasWritten(len);
}

void Buffer::Append(const Buffer& buff) {//添加另一个buff的数据
Append(buff.Peek(), buff.ReadableBytes());
}

void Buffer::EnsureWriteable(size_t len) {//确保这么大的长度能写
if(WritableBytes() < len) {//
MakeSpace_(len);//扩容
}
assert(WritableBytes() >= len);
}

ssize_t Buffer::ReadFd(int fd, int* saveErrno) {//接收
char buff[65535];//如果第一个缓冲区填不满,就用到这个缓冲区
struct iovec iov[2];
const size_t writable = WritableBytes();
/* 分散读, 保证数据全部读完 */
iov[0].iov_base = BeginPtr_() + writePos_;//定位可写的地方,不就是beginwrite()吗
iov[0].iov_len = writable;
iov[1].iov_base = buff;
iov[1].iov_len = sizeof(buff);

const ssize_t len = readv(fd, iov, 2);//2是指iovec结构的个数,返回值是有符号整型
if(len < 0) {
*saveErrno = errno;
}
else if(static_cast<size_t>(len) <= writable) {//转为无符号整型,长度是一样的
writePos_ += len;//小于可写的地方就更新当前的buffer
}
else {//写的超出了buffer可写的空间
writePos_ = buffer_.size();//更新
Append(buff, len - writable);//添加buff的数据,会扩大buffer的空间
}
return len;
}

ssize_t Buffer::WriteFd(int fd, int* saveErrno) {//写出
size_t readSize = ReadableBytes();
ssize_t len = write(fd, Peek(), readSize);//从当前开始(peek),写入准备好的数据
if(len < 0) {
*saveErrno = errno;
return len;
}
readPos_ += len;//更新
return len;
}
//char* 返回指向的字符串的首地址、也可以返回第一个字符、也可以返回整个字符串
char* Buffer::BeginPtr_() {//指向第一个char的地址
return &*buffer_.begin();//*begin()取第一个字符,&取地址
}

const char* Buffer::BeginPtr_() const {//区别是返回值要不要当常量处理
return &*buffer_.begin();
}

void Buffer::MakeSpace_(size_t len) {//扩容函数
if(WritableBytes() + PrependableBytes() < len) {//如果可写和已读都小于len,就必须重新开辟空间
buffer_.resize(writePos_ + len + 1);//resize
}
else {//否则,可以把已读的覆盖
size_t readable = ReadableBytes();//准备好的数据大小
std::copy(BeginPtr_() + readPos_, BeginPtr_() + writePos_, BeginPtr_());//把中间未读的,从头开始覆盖
readPos_ = 0;//已读为0
writePos_ = readPos_ + readable;//写的位置是准备好的数据的位置
assert(readable == ReadableBytes());
}
}

定时器

注意:这里没有加锁,上层的调用要加锁

chrono可以稍微参考下:(29条消息) C++11的chrono库,可实现毫秒微秒级定时_oncealong的博客-CSDN博客_chrono sleep。里面提了三种类型,虽然不详细。

  • std::chrono::high_resolution_clock:high_resolution_clock只不过是system_clock或者steady_clock的typedef。用于获取时间点。
    • std::chrono::system_clock 它表示当前的系统时钟,系统中运行的所有进程使用now()得到的时间是一致的。
    • std::chrono::steady_clock 为了表示稳定的时间间隔,后一次调用now()得到的时间总是比前一次的值大。用在需要得到时间间隔,并且这个时间间隔不会因为修改系统时间而受影响的场景;它是单调的时钟,相当于教练手中的秒表;只会增长,适合用于记录程序耗时,他表示的时钟是不能设置的。
    • 可以使用now()方法取得时间,是一个纳秒,相对系统启动的时间多少。一般用time_point:std::chrono::high_resolution_clock::time_point t1=std::chrono::high_resolution_clock::now();或者auto t1=std::chrono::high_resolution_clock::now();
  • std::chrono::milliseconds:表示毫秒,是一个时间间隔。
  • 在代码里面,now()+MS(timeout)被赋值到high_resolution_clock的time_point上,毫秒会转换为纳秒加上去。
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
/*
* @Author : mark
* @Date : 2020-06-17
* @copyleft Apache 2.0
*/
#ifndef HEAP_TIMER_H
#define HEAP_TIMER_H

#include <queue>
#include <unordered_map>
#include <time.h>
#include <algorithm>
#include <arpa/inet.h>
#include <functional>
#include <assert.h>
#include <chrono>
#include "../log/log.h"

typedef std::function<void()> TimeoutCallBack;
typedef std::chrono::high_resolution_clock Clock;//时钟
typedef std::chrono::milliseconds MS;//时间间隔
typedef Clock::time_point TimeStamp;//时钟内的时间点,获取now()方法结果

struct TimerNode {//时间结构
int id;//这个id用来给哈希表映射,这样查找时间是O(1),通过id映射到位置。
TimeStamp expires;//时间点
TimeoutCallBack cb;//回调函数
bool operator<(const TimerNode& t) {
return expires < t.expires;//比较
}
};
class HeapTimer {
public:
HeapTimer() { heap_.reserve(64); }//先指定vector有64个空间,其他情况下会两倍两倍的扩容

~HeapTimer() { clear(); }

void adjust(int id, int newExpires);

void add(int id, int timeOut, const TimeoutCallBack& cb);

void doWork(int id);

void clear();

void tick();

void pop();

int GetNextTick();

private:
//用size_t作为索引
void del_(size_t i);

void siftup_(size_t i);

bool siftdown_(size_t index, size_t n);

void SwapNode_(size_t i, size_t j);

std::vector<TimerNode> heap_;//用vector实现堆,是一个小顶堆

std::unordered_map<int, size_t> ref_;//哈希表,i->size_t
};

#endif //HEAP_TIMER_H

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
/*
* @Author : mark
* @Date : 2020-06-17
* @copyleft Apache 2.0
*/
#include "heaptimer.h"

void HeapTimer::siftup_(size_t i) {//向上过滤,用于插入节点
assert(i >= 0 && i < heap_.size());
size_t j = (i - 1) / 2;
while(j >= 0) {
if(heap_[j] < heap_[i]) { break; }
SwapNode_(i, j);
i = j;
j = (i - 1) / 2;
}
}

void HeapTimer::SwapNode_(size_t i, size_t j) {//交换节点的辅助函数
assert(i >= 0 && i < heap_.size());
assert(j >= 0 && j < heap_.size());
std::swap(heap_[i], heap_[j]);//交换vector元素
ref_[heap_[i].id] = i;//更改哈希表的映射
ref_[heap_[j].id] = j;
}

bool HeapTimer::siftdown_(size_t index, size_t n) {//向下过滤,用于建堆和删除顶点。这里是左闭右开的,n是取不到的右边界
assert(index >= 0 && index < heap_.size());
assert(n >= 0 && n <= heap_.size());
size_t i = index;
size_t j = i * 2 + 1;
while(j < n) {
if(j + 1 < n && heap_[j + 1] < heap_[j]) j++;
if(heap_[i] < heap_[j]) break;
SwapNode_(i, j);
i = j;
j = i * 2 + 1;
}
return i > index;//如果向下过滤了就返回true
}

void HeapTimer::add(int id, int timeout, const TimeoutCallBack& cb) {//插入节点,关联回调函数
assert(id >= 0);
size_t i;
if(ref_.count(id) == 0) {
/* 新节点:堆尾插入,调整堆 */
i = heap_.size();
ref_[id] = i;//先放i处
//结构体可以struct A = {...},调用默认构造函数,但必须把所有成员都赋值。常见的是struct A; A.x = ...逐个赋值
heap_.push_back({id, Clock::now() + MS(timeout), cb});//放i处,调用默认构造函数
siftup_(i);//向上过滤
}
else {//如果原来就有这个节点,说明没到时,重设时间,调整一下即可
/* 已有结点:调整堆 */
i = ref_[id];//获得位置
heap_[i].expires = Clock::now() + MS(timeout);//调整
heap_[i].cb = cb;
if(!siftdown_(i, heap_.size())) {//调整之后看看向上还是向下,如果不用向下过滤,那就向上过滤;如果向下过滤了,就不用向上了
siftup_(i);
}
}
}

void HeapTimer::doWork(int id) {
/* 删除指定id结点,并触发回调函数 */
if(heap_.empty() || ref_.count(id) == 0) {
return;
}
size_t i = ref_[id];
TimerNode node = heap_[i];//拷贝节点,如果是串行的话,是不是不需要拷贝,反正是调用完再删除
node.cb();//调用回调函数
del_(i);//删除
}

void HeapTimer::del_(size_t index) {
/* 删除指定位置的结点 */
assert(!heap_.empty() && index >= 0 && index < heap_.size());
/* 将要删除的结点换到队尾,然后调整堆 */
size_t i = index;
size_t n = heap_.size() - 1;//下标=大小-1
assert(i <= n);
if(i < n) {//删的不是最后一个元素就交换
SwapNode_(i, n);//把目前最后的元素换到前面去,此时要删的元素放到了最后
if(!siftdown_(i, n)) {//然后要调整这个元素的位置,先试一下向下过滤
siftup_(i);//不向下过滤的话就向上过滤。这里为什么要向上过滤呢?因为堆的兄弟之间没有关系,大堆有两个子堆,如果交换到另一个子堆就可能要向上
}
}
/* 队尾元素删除 */
ref_.erase(heap_.back().id);
heap_.pop_back();
}

void HeapTimer::adjust(int id, int timeout) {
/* 调整指定id的结点 */
assert(!heap_.empty() && ref_.count(id) > 0);
heap_[ref_[id]].expires = Clock::now() + MS(timeout);
siftdown_(ref_[id], heap_.size());//调整只可能更大,向下过滤
}

void HeapTimer::tick() {//这里的tick()时间复杂度要比链表形式的高
/* 清除超时结点 */
if(heap_.empty()) {
return;
}
while(!heap_.empty()) {
TimerNode node = heap_.front();//取顶
//这里先用预设时间-当前时间,结果是一个纳秒级的时间间隔,用间隔转换转到毫秒级,调用count(),它的作用是返回当前级别还有多少ticks(单位时间)
//比如3ms就有3ticks(在毫秒级下),因此这里是忽略毫秒级以下的数,只有剩余1毫秒及以上才不算超时。这是因为设定的超时时间是毫秒的,当然只看毫秒
if(std::chrono::duration_cast<MS>(node.expires - Clock::now()).count() > 0) { //等于0或小于0都超时
break;
}
node.cb();//超时,调用回调函数
pop();//删除顶部
}
}

void HeapTimer::pop() {
assert(!heap_.empty());
del_(0);//删除0号位置的节点
}

void HeapTimer::clear() {//清除
ref_.clear();
heap_.clear();
}

int HeapTimer::GetNextTick() {//看未超时的顶点剩下多少ticks
tick();//处理完超时的节点
size_t res = -1;
if(!heap_.empty()) {//如果非空
res = std::chrono::duration_cast<MS>(heap_.front().expires - Clock::now()).count();//看顶点还剩多少ticks(毫秒级别下)
if(res < 0) { res = 0; }//负数说明预设时间小于当前时间,也就是时间间隔是负的,说明超时了
}
return res;//-1说明空了,0说明超时了,大于0说明剩下的ticks
}

HTTP

处理响应

涉及到一个string.data(),看到比较好的文章里面提到了一点:

为什么C语言风格的字符串要以’\0’结尾,C++(string)可以不要?

c语言用char*指针作为字符串时,在读取字符串时需要一个特殊字符0来标记指针的结束位置,也就是通常认为的字符串结束标记。而c++语言则是面向对象的(string),长度信息直接被存储在了对象的成员中,读取字符串可以直接根据这个长度来读取,所以就没必要需要结束标记了。而且结束标记也不利于读取字符串中夹杂0字符的字符串。


  • 首先会尝试把文件信息写入stat结构体,根据文件找不找得到、文件权限,得到对应的状态码。stat结构体主要是获得文件size
  • 如果状态码是404那些,就把路径和stat结构体修改为404那些html文件的路径,如果是200OK,就再不修改。
  • 然后添加状态行、头部信息
  • 最后添加文件内容信息:
    • 先根据文件路径打开文件,可能是404那些html,也可能是真的文件。如果打开失败,会返回一个file not found的html
    • 打开成功的话会尝试去内存映射,stat结构体的size在这有用。如果映射失败,也会返回一个file not found的html
    • 如果打开成功,会添加文件的长度信息,把内存地址指针保存,可通过接口调用。因为不会真正写入文件内容
    • 在添加文件长度信息后,顺便添加一个空行。
  • 没有写入内容,等待外部写入。
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
/*
* @Author : mark
* @Date : 2020-06-25
* @copyleft Apache 2.0
*/
#ifndef HTTP_RESPONSE_H
#define HTTP_RESPONSE_H

#include <unordered_map>
#include <fcntl.h> // open
#include <unistd.h> // close
#include <sys/stat.h> // stat
#include <sys/mman.h> // mmap, munmap

#include "../buffer/buffer.h"
#include "../log/log.h"

class HttpResponse {
public:
HttpResponse();
~HttpResponse();

void Init(const std::string& srcDir, std::string& path, bool isKeepAlive = false, int code = -1);
void MakeResponse(Buffer& buff);
void UnmapFile();
char* File();
size_t FileLen() const;
void ErrorContent(Buffer& buff, std::string message);
int Code() const { return code_; }

private:
void AddStateLine_(Buffer &buff);
void AddHeader_(Buffer &buff);
void AddContent_(Buffer &buff);

void ErrorHtml_();
std::string GetFileType_();

int code_;//响应状态码
bool isKeepAlive_;

std::string path_;//资源路径
std::string srcDir_;//资源文件夹路径

char* mmFile_; //指向内存映射后的文件的内存空间
struct stat mmFileStat_;//存储文件信息

//静态的且不允许修改
static const std::unordered_map<std::string, std::string> SUFFIX_TYPE;//把后缀类型映射到http的文件类型
static const std::unordered_map<int, std::string> CODE_STATUS;//把响应码映射到响应字符串
static const std::unordered_map<int, std::string> CODE_PATH;//把响应码映射到响应需要发送的html文件的路径
};


#endif //HTTP_RESPONSE_H

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
/*
* @Author : mark
* @Date : 2020-06-27
* @copyleft Apache 2.0
*/
#include "httpresponse.h"

using namespace std;
//静态成员定义,用{}构造整体,用{key,value}构造元素
const unordered_map<string, string> HttpResponse::SUFFIX_TYPE = {
{ ".html", "text/html" },
{ ".xml", "text/xml" },
{ ".xhtml", "application/xhtml+xml" },
{ ".txt", "text/plain" },
{ ".rtf", "application/rtf" },
{ ".pdf", "application/pdf" },
{ ".word", "application/nsword" },
{ ".png", "image/png" },
{ ".gif", "image/gif" },
{ ".jpg", "image/jpeg" },
{ ".jpeg", "image/jpeg" },
{ ".au", "audio/basic" },
{ ".mpeg", "video/mpeg" },
{ ".mpg", "video/mpeg" },
{ ".avi", "video/x-msvideo" },
{ ".gz", "application/x-gzip" },
{ ".tar", "application/x-tar" },
{ ".css", "text/css "},
{ ".js", "text/javascript "},
};

const unordered_map<int, string> HttpResponse::CODE_STATUS = {
{ 200, "OK" },
{ 400, "Bad Request" },
{ 403, "Forbidden" },
{ 404, "Not Found" },
};

const unordered_map<int, string> HttpResponse::CODE_PATH = {
{ 400, "/400.html" },
{ 403, "/403.html" },
{ 404, "/404.html" },
};

HttpResponse::HttpResponse() {
code_ = -1;
path_ = srcDir_ = "";
isKeepAlive_ = false;
mmFile_ = nullptr;
mmFileStat_ = { 0 };
};

HttpResponse::~HttpResponse() {
UnmapFile();//解除内存映射,不用参数,用mmFile_指针
}

void HttpResponse::Init(const string& srcDir, string& path, bool isKeepAlive, int code){
assert(srcDir != "");
if(mmFile_) { UnmapFile(); }//一个上层实例由一个线程控制,那么一个类实例可以多次init,多次的话就要把原来的映射解除
code_ = code;//传入的状态,会根据之后的文件打开/访问成功与否改变
isKeepAlive_ = isKeepAlive;
//访问的文件路径
path_ = path;
srcDir_ = srcDir;
mmFile_ = nullptr;
mmFileStat_ = { 0 };
}
//向buff写入响应信息
void HttpResponse::MakeResponse(Buffer& buff) {//传入一个buff,没有真正写入文件内容(file not found除外,自定义返回了一个html)
/* 判断请求的资源文件 */
//stat函数,向stat结构体中写入path指定的文件信息,成功返回0,失败返回-1
//S_ISDIR()函数的作用是判断一个路径是不是目录,st_mode表示了文件对应的模式:文件,目录等。函数返回0表示是文件,返回1是文件夹
if(stat((srcDir_ + path_).data(), &mmFileStat_) < 0 || S_ISDIR(mmFileStat_.st_mode)) {//如果请求的文件不存在或者是文件夹,就404not found
code_ = 404;
}
//st_mode是个32位的整型变量,不过现在的linux操作系统只用了低16位(估计是鉴于以后拓展的考虑)
//最低9位代表了文件的许可权限,它标识了文件所有者(owner)、组用户(group)、其他用户(other)的读(r)、写(w)、执行(x)权限。
//S_IROTH:00004(无符号八进制数):others have read permission
else if(!(mmFileStat_.st_mode & S_IROTH)) {//也即,如果其他用户没有读权限的话就返回403forbidden
code_ = 403;
}
else if(code_ == -1) { //如果上面都没有,且没有被init为400,那就是初始值-1,表示ok
code_ = 200;
}
ErrorHtml_();//如果是error状态,会把对应的html页面文件信息添加到stat结构体中,把路径改了,成功就不做任何事情
AddStateLine_(buff);//添加状态行
AddHeader_(buff);//添加头部信息
AddContent_(buff);//返回文件内容,会尝试真正地打开文件,映射到内存,但是没有写入buff,会把内存指针放到mmFile_,File()接口调用
}

char* HttpResponse::File() {//返回文件映射到内存的位置
return mmFile_;
}

size_t HttpResponse::FileLen() const {//返回文件的大小
return mmFileStat_.st_size;
}

void HttpResponse::ErrorHtml_() {//如果是200OK,就不做任何事情
if(CODE_PATH.count(code_) == 1) {//如果有响应码对应的html文件,count找到返回1,否则0
path_ = CODE_PATH.find(code_)->second;//find返回一个迭代器,first是key,second是value。
//感觉可以用CODE_PATH[code_],因为前面已经找到了才执行,虽说内部也会遍历去find。另外,如果直接用[],没有这个元素会插入
stat((srcDir_ + path_).data(), &mmFileStat_);//把这个错误页面文件信息保存到stat中
}
}

void HttpResponse::AddStateLine_(Buffer& buff) {//向buff添加状态头部
string status;
if(CODE_STATUS.count(code_) == 1) {//如果有code_对应的状态
status = CODE_STATUS.find(code_)->second;//获取状态字符串
}
else {//上面处理了200,404,403,如果code_不知道被赋值成什么了,就400
code_ = 400;
status = CODE_STATUS.find(400)->second;
}
buff.Append("HTTP/1.1 " + to_string(code_) + " " + status + "\r\n");
}

void HttpResponse::AddHeader_(Buffer& buff) {//添加头部信息
buff.Append("Connection: ");
if(isKeepAlive_) {
buff.Append("keep-alive\r\n");
buff.Append("keep-alive: max=6, timeout=120\r\n");
} else{
buff.Append("close\r\n");
}
buff.Append("Content-type: " + GetFileType_() + "\r\n");
}

void HttpResponse::AddContent_(Buffer& buff) {//添加返回内容
//打开文件,string.data()返回c式字符串指针,c++11后与c_str()等价,结尾加'\0'。
int srcFd = open((srcDir_ + path_).data(), O_RDONLY);//O_RDONLY表示只读
if(srcFd < 0) { //打开失败
ErrorContent(buff, "File NotFound!");
return;
}

/* 将文件映射到内存提高文件的访问速度
MAP_PRIVATE 建立一个写入时拷贝的私有映射*/
LOG_DEBUG("file path %s", (srcDir_ + path_).data());

//成功返回创建的映射区的首地址;失败返回宏MAP_FAILED,这个宏就是-1。mmap返回一个void*
//返回值用mmret指向,表示指向一个int类型,解指针时以int类型解析,找4个字节
int* mmRet = (int*)mmap(0, mmFileStat_.st_size, PROT_READ, MAP_PRIVATE, srcFd, 0);
if(*mmRet == -1) {//取int,如果是-1表示失败了
ErrorContent(buff, "File NotFound!");//这里的notfound,实际上打开了文件了,但是写进内存出错了
return;
}
//取地址,修改指向的类型,两个指针指向的地址起始相同,但现在解指针按照char的解析类型解析,找一个字节。
//多少个字节都无妨,因为munmap传入的地址参数类型是void*指针,只要首地址正确就好
mmFile_ = (char*)mmRet;
close(srcFd);//关闭文件
buff.Append("Content-length: " + to_string(mmFileStat_.st_size) + "\r\n\r\n");//只添加内容长度,两个\r\n,后面那个表示空行
}

void HttpResponse::UnmapFile() {
if(mmFile_) {
munmap(mmFile_, mmFileStat_.st_size);//解除映射
mmFile_ = nullptr;
}
}

string HttpResponse::GetFileType_() {
/* 判断文件类型 */
//find_last_of返回最后一个.的位置,就是后缀类型前面那个.。逆向查找,返回的是下标
//size_type是string的长度表示方式,不同的机器大小不同,为了匹配机器上string的最大长度。因此找位置、长度这些要用size_type
string::size_type idx = path_.find_last_of('.');//找不到返回string::npos,表示不存在位置,值是-1
if(idx == string::npos) {
return "text/plain";
}
string suffix = path_.substr(idx);//从.开始返回后缀
if(SUFFIX_TYPE.count(suffix) == 1) {//有相应的类型就返回
return SUFFIX_TYPE.find(suffix)->second;
}
return "text/plain";//没有相应类型就返回这个
}

void HttpResponse::ErrorContent(Buffer& buff, string message) //自定义错误信息,在添加内容时遇到错误就返回这个html
{
string body;
string status;
body += "<html><title>Error</title>";
body += "<body bgcolor=\"ffffff\">";
if(CODE_STATUS.count(code_) == 1) {
status = CODE_STATUS.find(code_)->second;
} else {
status = "Bad Request";
}
body += to_string(code_) + " : " + status + "\n";
body += "<p>" + message + "</p>";
body += "<hr><em>TinyWebServer</em></body></html>";

buff.Append("Content-length: " + to_string(body.size()) + "\r\n\r\n");
buff.Append(body);
}

处理请求

用到正则,语法:正则表达式 – 语法 | 菜鸟教程 (runoob.com)

几种用法:C++ regex库的三种正则表达式操作 - 上官栋 - 博客园 (cnblogs.com)

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
/*
* @Author : mark
* @Date : 2020-06-25
* @copyleft Apache 2.0
*/
#ifndef HTTP_REQUEST_H
#define HTTP_REQUEST_H

#include <unordered_map>
#include <unordered_set>
#include <string>
#include <regex>
#include <errno.h>
#include <mysql/mysql.h> //mysql

#include "../buffer/buffer.h"
#include "../log/log.h"
#include "../pool/sqlconnpool.h"
#include "../pool/sqlconnRAII.h"

class HttpRequest {
public:
enum PARSE_STATE {//解析状态
REQUEST_LINE,
HEADERS,
BODY,
FINISH,
};

enum HTTP_CODE {
NO_REQUEST = 0,
GET_REQUEST,
BAD_REQUEST,
NO_RESOURSE,
FORBIDDENT_REQUEST,
FILE_REQUEST,
INTERNAL_ERROR,
CLOSED_CONNECTION,
};

HttpRequest() { Init(); }
~HttpRequest() = default;

void Init();
bool parse(Buffer& buff);

std::string path() const;
std::string& path();
std::string method() const;
std::string version() const;
std::string GetPost(const std::string& key) const;
std::string GetPost(const char* key) const;

bool IsKeepAlive() const;

/*
todo
void HttpConn::ParseFormData() {}
void HttpConn::ParseJson() {}
*/

private:
bool ParseRequestLine_(const std::string& line);
void ParseHeader_(const std::string& line);
void ParseBody_(const std::string& line);

void ParsePath_();
void ParsePost_();
void ParseFromUrlencoded_();

static bool UserVerify(const std::string& name, const std::string& pwd, bool isLogin);

PARSE_STATE state_;//描述目前的解析状态
std::string method_, path_, version_, body_;
std::unordered_map<std::string, std::string> header_;//保存信息,描述->参数
std::unordered_map<std::string, std::string> post_;

static const std::unordered_set<std::string> DEFAULT_HTML;
static const std::unordered_map<std::string, int> DEFAULT_HTML_TAG;
static int ConverHex(char ch);
};


#endif //HTTP_REQUEST_H

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
/*
* @Author : mark
* @Date : 2020-06-26
* @copyleft Apache 2.0
*/
#include "httprequest.h"
using namespace std;

const unordered_set<string> HttpRequest::DEFAULT_HTML{
"/index", "/register", "/login",
"/welcome", "/video", "/picture", };

const unordered_map<string, int> HttpRequest::DEFAULT_HTML_TAG {
{"/register.html", 0}, {"/login.html", 1}, };

void HttpRequest::Init() {
method_ = path_ = version_ = body_ = "";
state_ = REQUEST_LINE;
header_.clear();
post_.clear();
}

bool HttpRequest::IsKeepAlive() const {
if(header_.count("Connection") == 1) {//如果有头部有这个参数,就去找要不要keep
return header_.find("Connection")->second == "keep-alive" && version_ == "1.1";
}
return false;
}
//解析
bool HttpRequest::parse(Buffer& buff) {
const char CRLF[] = "\r\n";
if(buff.ReadableBytes() <= 0) {
return false;
}
while(buff.ReadableBytes() && state_ != FINISH) {
//search:查找 [first1, last1) 范围内第一个 [first2, last2) 子序列,返回指向first2的首地址。未找到就返回last1,请求数据就没/r/n
const char* lineEnd = search(buff.Peek(), buff.BeginWriteConst(), CRLF, CRLF + 2);//每次获取一行
std::string line(buff.Peek(), lineEnd);//拷贝一行,左闭右开
switch(state_)//看看现在是在解析什么
{
case REQUEST_LINE://请求行
if(!ParseRequestLine_(line)) {//会根据要访问什么资源把path弄好
return false;//有数据但是请求行都没准备好
}
ParsePath_();//解析path
break;
case HEADERS:
ParseHeader_(line);//不断解析头部
if(buff.ReadableBytes() <= 2) {//解析完一个头部后会判断是不是/r/n,两个字节就是空行,没有请求数据,就结束
state_ = FINISH;
}
break;
case BODY:
ParseBody_(line);
break;
default:
break;
}
if(lineEnd == buff.BeginWrite()) { break; }//空了,提前结束,避免buff出错,因为请求数据可能没有/r/n
buff.RetrieveUntil(lineEnd + 2);//过滤/r/n
}
LOG_DEBUG("[%s], [%s], [%s]", method_.c_str(), path_.c_str(), version_.c_str());
return true;
}
//解析请求文件路径
void HttpRequest::ParsePath_() {
if(path_ == "/") {//输入ip地址最后只有一个/,返回的就是主页面
path_ = "/index.html";
}
else {
for(auto &item: DEFAULT_HTML) {
if(item == path_) {//遍历集合
path_ += ".html";
break;
}
}
}
}

//看一个请求行的实例:GET /562f25980001b1b106000338.jpg HTTP/1.1
//第一个^表示开始,匹配的字符串开始必须是[^ ]*,这个表示匹配除了空格的所有,遇到空格结束。然后会跟一个空格,再匹配下一个连续的串,遇到空格为止
//然后匹配一个空格,匹配HTTP/,再匹配除了空格的内容,如果有/r/n,也会匹配。最后$表示结束,后面不再匹配
bool HttpRequest::ParseRequestLine_(const string& line) {
regex patten("^([^ ]*) ([^ ]*) HTTP/([^ ]*)$");//匹配模式
smatch subMatch;//存储结果,每个()中是一个子表达式,第0个参数是完整结果,1-n是()中的结果
if(regex_match(line, subMatch, patten)) {//严格要求各参数之间只有一个空格
method_ = subMatch[1];
path_ = subMatch[2];
version_ = subMatch[3];//说明传进来的line的/r/n被去掉了
state_ = HEADERS;//接下来解析请求头
return true;
}
LOG_ERROR("RequestLine Error");
return false;
}
//解析请求头,比如host: ... 或者host:...
//首先匹配冒号前面的,到冒号停下,然后匹配冒号,然后是一个空格,问号表示这个空格可以匹配0次或1次,因为报文中空格可以0次或多次
//然后匹配剩下的除换行符之外的所有字符,.相当于[^\n\r]。
void HttpRequest::ParseHeader_(const string& line) {
regex patten("^([^:]*): ?(.*)$");
smatch subMatch;
if(regex_match(line, subMatch, patten)) {//匹配成功
header_[subMatch[1]] = subMatch[2];//添加头部信息
}
else {//失败就是到空行了,准备解析请求体。上层会判断是否有请求体,没有就结束
state_ = BODY;
}
}
//解析请求体,就是post的请求数据
void HttpRequest::ParseBody_(const string& line) {
body_ = line;//拷贝
ParsePost_();//调用解析post
state_ = FINISH;//结束咯
LOG_DEBUG("Body:%s, len:%d", line.c_str(), line.size());
}

int HttpRequest::ConverHex(char ch) {//把一个十六进制的字符转为int数字
if(ch >= 'A' && ch <= 'F') return ch -'A' + 10;//+10是因为A本身在十六进制代表10
if(ch >= 'a' && ch <= 'f') return ch -'a' + 10;
return ch;//是不是忘了-'0'
}

void HttpRequest::ParsePost_() {
//application/x-www-form-urlencoded是最常见的 POST 提交数据方式。这里只解析这种格式
//请求数据实例:name=Professional%20Ajax&publisher=Wiley
if(method_ == "POST" && header_["Content-Type"] == "application/x-www-form-urlencoded") {
ParseFromUrlencoded_();//把body解码
if(DEFAULT_HTML_TAG.count(path_)) {//login的话,post的url就是当前请求的页面,就能解析到是/login.html,得到对应tag
int tag = DEFAULT_HTML_TAG.find(path_)->second;
LOG_DEBUG("Tag:%d", tag);
if(tag == 0 || tag == 1) {
bool isLogin = (tag == 1);//看是登录还是注册
if(UserVerify(post_["username"], post_["password"], isLogin)) {//处理登录和处理注册
path_ = "/welcome.html";
}
else {
path_ = "/error.html";
}
}
}
}
}

//name1=value1&name2=value2&name3=value3&.....&nameN=valueN
//用"+"取代空字符
//非数字, 字母用%HH格式进行替换, 其中HH是两位16进制数字, 表示被替换字符的ASCII码(例如"?"会被替换成"%3F", 对应十进制数是63,也就是问号对应的ASCII值)
//换行符用CR LF字符对表示, 对应的值是"%0D%0A";
void HttpRequest::ParseFromUrlencoded_() {
if(body_.size() == 0) { return; }

string key, value;
int num = 0;
int n = body_.size();
int i = 0, j = 0;

for(; i < n; i++) {//逐个解析
char ch = body_[i];
switch (ch) {
case '='://如果是等号,key就是j-i-1这一段,
key = body_.substr(j, i - j);//长度是i-j
j = i + 1;//下一个str起始
break;
case '+'://如果是+,换回空格
body_[i] = ' ';
break;
case '%'://后面跟两个十六进制的字符
num = ConverHex(body_[i + 1]) * 16 + ConverHex(body_[i + 2]);//转化为对应的ascii码
body_[i + 2] = num % 10 + '0';
body_[i + 1] = num / 10 + '0';//并不是转换为ascii对应的符号,转换为数字对应的字符形式而已,就是16进制转十进制
i += 2;
break;
case '&'://如果是&,就得到value
value = body_.substr(j, i - j);
j = i + 1;
post_[key] = value;//存储
LOG_DEBUG("%s = %s", key.c_str(), value.c_str());
break;
default:
break;
}
}
assert(j <= i);//处理最后一个value,它没有&
if(post_.count(key) == 0 && j < i) {
value = body_.substr(j, i - j);
post_[key] = value;
}
}

bool HttpRequest::UserVerify(const string &name, const string &pwd, bool isLogin) {
if(name == "" || pwd == "") { return false; }//空的话返回错误
LOG_INFO("Verify name:%s pwd:%s", name.c_str(), pwd.c_str());//否则就记录
MYSQL* sql;
SqlConnRAII(&sql, SqlConnPool::Instance());//初始化连接数据库,返回全局的静态的连接池
assert(sql);

bool flag = false;
unsigned int j = 0;
char order[256] = { 0 };
MYSQL_FIELD *fields = nullptr;
MYSQL_RES *res = nullptr;

if(!isLogin) { flag = true; }//如果是注册
/* 查询用户及密码 */
snprintf(order, 256, "SELECT username, password FROM user WHERE username='%s' LIMIT 1", name.c_str());//sql语句,查一个
LOG_DEBUG("%s", order);

if(mysql_query(sql, order)) { //执行语句,成功返回0,错误返回非0
mysql_free_result(res);//错误的话释放结果集并返回
return false;
}

res = mysql_store_result(sql);//完整的结果集
j = mysql_num_fields(res); //返回结果集中的列数
fields = mysql_fetch_fields(res);//返回所有字段结构的数组

while(MYSQL_ROW row = mysql_fetch_row(res)) {//遍历行,实际上只有一行,但这样可以取出行
LOG_DEBUG("MYSQL ROW: %s %s", row[0], row[1]);
string password(row[1]);
// 能select到说明又对应的username,看是登录还是注册
if(isLogin) {
if(pwd == password) { flag = true; }//标记成功,可以直接return的
else {
flag = false;
LOG_DEBUG("pwd error!");
//可以直接return的
}
}
//如果是注册,注意能进到这个while说明取出了row,就说明前面res select到了一个username,重名了
else {
flag = false;
LOG_DEBUG("user used!");
//可以直接return的
}
}
mysql_free_result(res);//释放结果集使用的内存,store后要释放

/* 注册行为 且 用户名未被使用*/
if(!isLogin && flag == true) {
LOG_DEBUG("regirster!");
bzero(order, 256);
snprintf(order, 256,"INSERT INTO user(username, password) VALUES('%s','%s')", name.c_str(), pwd.c_str());//插入表
LOG_DEBUG( "%s", order);
if(mysql_query(sql, order)) {
LOG_DEBUG( "Insert error!");
flag = false;
}
flag = true;
}

SqlConnPool::Instance()->FreeConn(sql);//这行不用,使用了RAII机制了
LOG_DEBUG( "UserVerify success!!");
return flag;
}

std::string HttpRequest::path() const{
return path_;
}

std::string& HttpRequest::path(){//外部可修改path_
return path_;
}
std::string HttpRequest::method() const {
return method_;
}

std::string HttpRequest::version() const {
return version_;
}
//接口函数,供外部使用,用于获取解析请求体后获得的参数
std::string HttpRequest::GetPost(const std::string& key) const {
assert(key != "");
if(post_.count(key) == 1) {
return post_.find(key)->second;
}
return "";
}

std::string HttpRequest::GetPost(const char* key) const {
assert(key != nullptr);
if(post_.count(key) == 1) {
return post_.find(key)->second;
}
return "";
}

上层调用

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
/*
* @Author : mark
* @Date : 2020-06-15
* @copyleft Apache 2.0
*/

#ifndef HTTP_CONN_H
#define HTTP_CONN_H

#include <sys/types.h>
#include <sys/uio.h> // readv/writev
#include <arpa/inet.h> // sockaddr_in
#include <stdlib.h> // atoi()
#include <errno.h>

#include "../log/log.h"
#include "../pool/sqlconnRAII.h"
#include "../buffer/buffer.h"
#include "httprequest.h"
#include "httpresponse.h"

class HttpConn {
public:
HttpConn();

~HttpConn();

void init(int sockFd, const sockaddr_in& addr);

ssize_t read(int* saveErrno);

ssize_t write(int* saveErrno);

void Close();

int GetFd() const;

int GetPort() const;

const char* GetIP() const;

sockaddr_in GetAddr() const;

bool process();

int ToWriteBytes() {
return iov_[0].iov_len + iov_[1].iov_len;
}

bool IsKeepAlive() const {
return request_.IsKeepAlive();
}

static bool isET;//是否是ET触发模式
static const char* srcDir;
static std::atomic<int> userCount;

private:

int fd_;
struct sockaddr_in addr_;//internet环境下套接字的地址形式

bool isClose_;

int iovCnt_;//根据有没有文件要传,有的话就是两个。打开文件失败的话,会返回自定义的html,这个html是写到buff的,此时没有文件要传
struct iovec iov_[2];//0发送响应报文,1发送响应文件

Buffer readBuff_; // 读缓冲区
Buffer writeBuff_; // 写缓冲区

HttpRequest request_;
HttpResponse response_;
};


#endif //HTTP_CONN_H

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
/*
* @Author : mark
* @Date : 2020-06-15
* @copyleft Apache 2.0
*/
#include "httpconn.h"
using namespace std;

const char* HttpConn::srcDir;
std::atomic<int> HttpConn::userCount;
bool HttpConn::isET;

HttpConn::HttpConn() {
fd_ = -1;
addr_ = { 0 };
isClose_ = true;
};

HttpConn::~HttpConn() {
Close();
};

void HttpConn::init(int fd, const sockaddr_in& addr) {
assert(fd > 0);
userCount++;
addr_ = addr;
fd_ = fd;
writeBuff_.RetrieveAll();
readBuff_.RetrieveAll();
isClose_ = false;
LOG_INFO("Client[%d](%s:%d) in, userCount:%d", fd_, GetIP(), GetPort(), (int)userCount);
}

void HttpConn::Close() {
response_.UnmapFile();
if(isClose_ == false){
isClose_ = true;
userCount--;
close(fd_);
LOG_INFO("Client[%d](%s:%d) quit, UserCount:%d", fd_, GetIP(), GetPort(), (int)userCount);
}
}

int HttpConn::GetFd() const {
return fd_;
};

struct sockaddr_in HttpConn::GetAddr() const {
return addr_;
}

//ntoa:network to ascii,将网络地址转换成“.”点隔的字符串格式
//<arpa/inet.h>,char *inet_ntoa (struct in_addr);参数是结构体
//相反的函数是inet_addr,讲ip转换为长整型,参数是ip字符串,如addr_.sin_addr.s_addr = inet_addr("132.241.5.10");
const char* HttpConn::GetIP() const {
return inet_ntoa(addr_.sin_addr);
}

int HttpConn::GetPort() const {
return addr_.sin_port;//sin_port存储端口号(使用网络字节顺序)
}

ssize_t HttpConn::read(int* saveErrno) {//读数据到自己的缓冲区
ssize_t len = -1;
do {
len = readBuff_.ReadFd(fd_, saveErrno);//调用读缓冲区
if (len <= 0) {
break;
}
} while (isET);//如果是ET模式就一直读取直到len==0,LT模式就读一次就结束
return len;
}

ssize_t HttpConn::write(int* saveErrno) {//iov里的数据写出去
ssize_t len = -1;
do {
len = writev(fd_, iov_, iovCnt_);//写多个非连续缓冲区(聚集写),成功返回字节数,失败返回-1,因此用ssize_t,是signed的
if(len <= 0) {
*saveErrno = errno;
break;
}
if(iov_[0].iov_len + iov_[1].iov_len == 0) { break; } /* 传输结束 *///也就是TuWriteBytes()==0
else if(static_cast<size_t>(len) > iov_[0].iov_len) {//第一个缓冲区以写完
iov_[1].iov_base = (uint8_t*) iov_[1].iov_base + (len - iov_[0].iov_len);//更新第二个缓冲区剩下的
iov_[1].iov_len -= (len - iov_[0].iov_len);
if(iov_[0].iov_len) {//然后更新第一个缓冲区为0
writeBuff_.RetrieveAll();//写缓冲区已经发送完毕
iov_[0].iov_len = 0;
}
}
else {//c++中指针相互赋值要显式转换,void*可接受任意类型的赋值(不必转换);反过来不行,void*要给其他变量赋值要显示转换
iov_[0].iov_base = (uint8_t*)iov_[0].iov_base + len;//typedef unsigned char uint8_t;
iov_[0].iov_len -= len;
writeBuff_.Retrieve(len);//已经发送了len长度
}
} while(isET || ToWriteBytes() > 10240);//ET模式一直写,或者有太多字节要写了,就多写几次
return len;
}

bool HttpConn::process() {//解析请求数据,并把响应数据放到iovec结构体
request_.Init();//处理请求初始化
if(readBuff_.ReadableBytes() <= 0) {//如果一点数据都没接收到
return false;
}
else if(request_.parse(readBuff_)) {//有数据就处理,如果解析请求行错误就false
LOG_DEBUG("%s", request_.path().c_str());//解析请求行成功,后面数据有无无所谓。get的话请求行就有路径,post的话会检查请求体,没有就返回error
response_.Init(srcDir, request_.path(), request_.IsKeepAlive(), 200);//然后初始化响应
} else {
response_.Init(srcDir, request_.path(), false, 400);//请求错误,返回400 bad request
}

response_.MakeResponse(writeBuff_);//响应数据写到缓冲区
/* 响应头 */
iov_[0].iov_base = const_cast<char*>(writeBuff_.Peek());//赋予内存起始地址,用const_cast消去const
iov_[0].iov_len = writeBuff_.ReadableBytes();//大小
iovCnt_ = 1;

/* 文件 */
if(response_.FileLen() > 0 && response_.File()) {//文件不写到缓冲区,直接返回文件内存映射的指针,给iov1
iov_[1].iov_base = response_.File();
iov_[1].iov_len = response_.FileLen();
iovCnt_ = 2;
}
LOG_DEBUG("filesize:%d, %d to %d", response_.FileLen() , iovCnt_, ToWriteBytes());
return true;
}

服务器顶层

事务处理

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
/*
* @Author : mark
* @Date : 2020-06-15
* @copyleft Apache 2.0
*/
#ifndef EPOLLER_H
#define EPOLLER_H

#include <sys/epoll.h> //epoll_ctl()
#include <fcntl.h> // fcntl()
#include <unistd.h> // close()
#include <assert.h> // close()
#include <vector>
#include <errno.h>

class Epoller {
public:
explicit Epoller(int maxEvent = 1024);

~Epoller();

bool AddFd(int fd, uint32_t events);

bool ModFd(int fd, uint32_t events);

bool DelFd(int fd);

int Wait(int timeoutMs = -1);

int GetEventFd(size_t i) const;

uint32_t GetEvents(size_t i) const;

private:
int epollFd_;

std::vector<struct epoll_event> events_;//存放监听到的事件
};

#endif //EPOLLER_H

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
/*
* @Author : mark
* @Date : 2020-06-19
* @copyleft Apache 2.0
*/

#include "epoller.h"

Epoller::Epoller(int maxEvent):epollFd_(epoll_create(512)), events_(maxEvent){//构造内核事件表描述符,以及事件集合
assert(epollFd_ >= 0 && events_.size() > 0);
}

Epoller::~Epoller() {
close(epollFd_);//关闭句柄
}

bool Epoller::AddFd(int fd, uint32_t events) {
if(fd < 0) return false;
epoll_event ev = {0};//创建一个epoll_event,前面的vector存放事件,这里是为了描述事件的类型,关联fd。不初始化也可以:epoll_event ev;
ev.data.fd = fd;//关联fd
ev.events = events;//上层设置好类型
return 0 == epoll_ctl(epollFd_, EPOLL_CTL_ADD, fd, &ev);//add,成功返回0
}

bool Epoller::ModFd(int fd, uint32_t events) {//和add差不多
if(fd < 0) return false;
epoll_event ev = {0};
ev.data.fd = fd;
ev.events = events;
return 0 == epoll_ctl(epollFd_, EPOLL_CTL_MOD, fd, &ev);//mod
}

bool Epoller::DelFd(int fd) {
if(fd < 0) return false;
epoll_event ev = {0};//可以不创建,epoll_ctl(epollFd_, EPOLL_CTL_DEL, fd, 0)
return 0 == epoll_ctl(epollFd_, EPOLL_CTL_DEL, fd, &ev);
}

//对于timeout:-1:永远等待;0:不等待直接返回,执行下面的代码;其他:在超时时间内没有事件发生,返回0,如果有事件发生立即返回
int Epoller::Wait(int timeoutMs) {//成功返回多少事件就绪,超时返回0,出错返回-1
return epoll_wait(epollFd_, &events_[0], static_cast<int>(events_.size()), timeoutMs);
}//&events_[0]等价于events;vector.size()返回类型是size_t,unsigned int转int

int Epoller::GetEventFd(size_t i) const {//调用wait后,从events事件集合中取出对应的可io的文件描述符
assert(i < events_.size() && i >= 0);
return events_[i].data.fd;
}

uint32_t Epoller::GetEvents(size_t i) const {//调用wait后,从events事件集合中取出对应的事件的类型描述
assert(i < events_.size() && i >= 0);
return events_[i].events;
}

顶层调用

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
/*
* @Author : mark
* @Date : 2020-06-17
* @copyleft Apache 2.0
*/
#ifndef WEBSERVER_H
#define WEBSERVER_H

#include <unordered_map>
#include <fcntl.h> // fcntl()
#include <unistd.h> // close()
#include <assert.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "epoller.h"
#include "../log/log.h"
#include "../timer/heaptimer.h"
#include "../pool/sqlconnpool.h"
#include "../pool/threadpool.h"
#include "../pool/sqlconnRAII.h"
#include "../http/httpconn.h"

class WebServer {
public:
WebServer(
int port, int trigMode, int timeoutMS, bool OptLinger,
int sqlPort, const char* sqlUser, const char* sqlPwd,
const char* dbName, int connPoolNum, int threadNum,
bool openLog, int logLevel, int logQueSize);

~WebServer();
void Start();

private:
bool InitSocket_();
void InitEventMode_(int trigMode);
void AddClient_(int fd, sockaddr_in addr);

void DealListen_();
void DealWrite_(HttpConn* client);
void DealRead_(HttpConn* client);

void SendError_(int fd, const char*info);
void ExtentTime_(HttpConn* client);
void CloseConn_(HttpConn* client);

void OnRead_(HttpConn* client);
void OnWrite_(HttpConn* client);
void OnProcess(HttpConn* client);

static const int MAX_FD = 65536;

static int SetFdNonblock(int fd);

int port_;
bool openLinger_;
int timeoutMS_; /* 毫秒MS */
bool isClose_;
int listenFd_;//fd是socket
char* srcDir_;

//监听是接受tcp连接,所谓的连接是指维护客户与服务器之间的数据交换
uint32_t listenEvent_;//监听模式,维护服务器的监听事件的类型
uint32_t connEvent_;//连接模式,维护客户端与服务器之间的连接事件的类型
//动态创建的,都用智能指针
std::unique_ptr<HeapTimer> timer_;
std::unique_ptr<ThreadPool> threadpool_;
std::unique_ptr<Epoller> epoller_;
std::unordered_map<int, HttpConn> users_;//一个用户的socket匹配一个连接
};


#endif //WEBSERVER_H

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
/*
* @Author : mark
* @Date : 2020-06-17
* @copyleft Apache 2.0
*/

#include "webserver.h"

using namespace std;

WebServer::WebServer(
int port, int trigMode, int timeoutMS, bool OptLinger,
int sqlPort, const char* sqlUser, const char* sqlPwd,
const char* dbName, int connPoolNum, int threadNum,
bool openLog, int logLevel, int logQueSize):
port_(port), openLinger_(OptLinger), timeoutMS_(timeoutMS), isClose_(false),
timer_(new HeapTimer()), threadpool_(new ThreadPool(threadNum)), epoller_(new Epoller())
{
srcDir_ = getcwd(nullptr, 256);//获取当前路径
assert(srcDir_);
strncat(srcDir_, "/resources/", 16);
HttpConn::userCount = 0;
HttpConn::srcDir = srcDir_;
SqlConnPool::Instance()->Init("localhost", sqlPort, sqlUser, sqlPwd, dbName, connPoolNum);

InitEventMode_(trigMode);
if(!InitSocket_()) { isClose_ = true;}

if(openLog) {
Log::Instance()->init(logLevel, "./log", ".log", logQueSize);
if(isClose_) { LOG_ERROR("========== Server init error!=========="); }
else {
LOG_INFO("========== Server init ==========");
LOG_INFO("Port:%d, OpenLinger: %s", port_, OptLinger? "true":"false");
LOG_INFO("Listen Mode: %s, OpenConn Mode: %s",
(listenEvent_ & EPOLLET ? "ET": "LT"),
(connEvent_ & EPOLLET ? "ET": "LT"));
LOG_INFO("LogSys level: %d", logLevel);
LOG_INFO("srcDir: %s", HttpConn::srcDir);
LOG_INFO("SqlConnPool num: %d, ThreadPool num: %d", connPoolNum, threadNum);
}
}
}

WebServer::~WebServer() {
close(listenFd_);
isClose_ = true;
free(srcDir_);
SqlConnPool::Instance()->ClosePool();
}

void WebServer::InitEventMode_(int trigMode) {
//对端正常断开连接(调用 close()),在服务器端会触发一个 epoll 事件,EPOLLRDHUP监听挂断事件,在底层完成
//EPOLLRDHUP:https://blog.csdn.net/midion9/article/details/49883063
//EPOLLHUP:https://blog.csdn.net/voidccc/article/details/8619632
listenEvent_ = EPOLLRDHUP;//说明要监听对端是否挂断
connEvent_ = EPOLLONESHOT | EPOLLRDHUP;//连接事务每次响应后要重新设置
switch (trigMode)
{
case 0:
break;
case 1:
connEvent_ |= EPOLLET;
break;
case 2:
listenEvent_ |= EPOLLET;
break;
case 3:
listenEvent_ |= EPOLLET;
connEvent_ |= EPOLLET;
break;
default:
listenEvent_ |= EPOLLET;
connEvent_ |= EPOLLET;
break;
}
HttpConn::isET = (connEvent_ & EPOLLET);
}

void WebServer::Start() {
int timeMS = -1; /* epoll wait timeout == -1 无事件将阻塞 */
if(!isClose_) { LOG_INFO("========== Server start =========="); }
while(!isClose_) {
if(timeoutMS_ > 0) {//设置了超时处理的话,每轮都处理超时的事件
timeMS = timer_->GetNextTick();//获取下一个定时器超时的剩余时间
}
int eventCnt = epoller_->Wait(timeMS);//等待事件触发,有事件触发立即返回,没有会等timeMS的时间
//等待的事件包括监听事件和读写事件
for(int i = 0; i < eventCnt; i++) {
/* 处理事件 */
int fd = epoller_->GetEventFd(i);//获取fd描述符
uint32_t events = epoller_->GetEvents(i);//获取时间的类型
if(fd == listenFd_) {//如果监听到一个新连接
DealListen_();
}
//如果是触发的事件是对端断开或者对端异常(ERR通常是服务器读写(自身采取行动)发现对方异常触发)
else if(events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {//异常事件
assert(users_.count(fd) > 0);//确定user内保存了这个描述符
CloseConn_(&users_[fd]);
}
else if(events & EPOLLIN) {//如果是读事件
assert(users_.count(fd) > 0);
DealRead_(&users_[fd]);
}
else if(events & EPOLLOUT) {//如果是写事件
assert(users_.count(fd) > 0);
DealWrite_(&users_[fd]);
} else {//非预期事件
LOG_ERROR("Unexpected event");
}
}
}
}

void WebServer::SendError_(int fd, const char*info) {
assert(fd > 0);
int ret = send(fd, info, strlen(info), 0);//向某个socket发生信息,送到对方的socket由对方处理这个信息
if(ret < 0) {
LOG_WARN("send error to client[%d] error!", fd);
}
close(fd);
}

void WebServer::CloseConn_(HttpConn* client) {//关闭连接
assert(client);//首先会有一个连接类维护这个连接
LOG_INFO("Client[%d] quit!", client->GetFd());
epoller_->DelFd(client->GetFd());//获取连接类维护的socket并从事件表中删除,并不关闭
client->Close();//关闭连接,这个会关闭fd。实际上连接类的析构也会close(),但在使用过程中要手动释放资源
}

void WebServer::AddClient_(int fd, sockaddr_in addr) {
assert(fd > 0);
//创建http连接
users_[fd].init(fd, addr);//不指定value的写法,会讲value以0初始化,然后这里再调用init
//创建计时器关联http连接
if(timeoutMS_ > 0) {//有超时处理的话,就绑定一个定时器,回调函数用来关闭连接。这种形式的回调函数不再需要是static
timer_->add(fd, timeoutMS_, std::bind(&WebServer::CloseConn_, this, &users_[fd]));//绑定参数,this是第一个参数,client是
}
//向内核事件表注册
epoller_->AddFd(fd, EPOLLIN | connEvent_);
//设置非阻塞
SetFdNonblock(fd);
LOG_INFO("Client[%d] in!", users_[fd].GetFd());
}

void WebServer::DealListen_() {
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
do {
int fd = accept(listenFd_, (struct sockaddr *)&addr, &len);//尝试通过listen socket连接,获取地址
if(fd <= 0) { return;}//连接失败,即没有数据了,返回
else if(HttpConn::userCount >= MAX_FD) {//请求过多,无法连接
SendError_(fd, "Server busy!");//向socket发送错误信息
LOG_WARN("Clients is full!");
return;
}
AddClient_(fd, addr);//添加连接
} while(listenEvent_ & EPOLLET);//ET模式要把事件全部处理,应该就是把socket缓冲区的地址全部读完
}

void WebServer::DealRead_(HttpConn* client) {//处理读事件
assert(client);
ExtentTime_(client);//有响应就刷新时间
threadpool_->AddTask(std::bind(&WebServer::OnRead_, this, client));//唤醒一个线程处理读事件
}

void WebServer::DealWrite_(HttpConn* client) {//处理写事件
assert(client);
ExtentTime_(client);
threadpool_->AddTask(std::bind(&WebServer::OnWrite_, this, client));
}

void WebServer::ExtentTime_(HttpConn* client) {//重新设置时间
assert(client);
if(timeoutMS_ > 0) { timer_->adjust(client->GetFd(), timeoutMS_); }
}

void WebServer::OnRead_(HttpConn* client) {//调用读
assert(client);
int ret = -1;
int readErrno = 0;
ret = client->read(&readErrno);//读取数据,把数据读到缓冲区
if(ret <= 0 && readErrno != EAGAIN) {//如果读不到数据且不是因为缓冲区空了,那么就异常,关闭连接
CloseConn_(client);
return;
}
OnProcess(client);//读完了就解析请求数据
}

void WebServer::OnProcess(HttpConn* client) {//解析数据
if(client->process()) {//如果解析成功了,无论是正常请求还是bad请求,通知内核事件输出,即修改成out
epoller_->ModFd(client->GetFd(), connEvent_ | EPOLLOUT);
} else {//缓冲区一点数据都没收到,,继续读
epoller_->ModFd(client->GetFd(), connEvent_ | EPOLLIN);
}
}

void WebServer::OnWrite_(HttpConn* client) {//调用写
assert(client);
int ret = -1;
int writeErrno = 0;
ret = client->write(&writeErrno);//传输数据
if(client->ToWriteBytes() == 0) {
/* 传输完成 */
if(client->IsKeepAlive()) {//如果是keep
OnProcess(client);//改成in
return;
}
}
else if(ret < 0) {//传输数据小于0
if(writeErrno == EAGAIN) {//如果是socket的发送缓存被占满,要继续写
/* 继续传输 */
epoller_->ModFd(client->GetFd(), connEvent_ | EPOLLOUT);//总是告知继续写,除非写完
return;
}
}
CloseConn_(client);//成功且不keep就关掉连接
}

/* Create listenFd */
bool WebServer::InitSocket_() {//初始化监听窗口,注入内核事件表
int ret;
struct sockaddr_in addr;
if(port_ > 65535 || port_ < 1024) {
LOG_ERROR("Port:%d error!", port_);
return false;
}
addr.sin_family = AF_INET;//AF_INET 表示 IPv4 地址
addr.sin_addr.s_addr = htonl(INADDR_ANY);//本函数将一个32位数从主机字节顺序转换成网络字节顺序,ANY泛指本机,监听所有网卡
addr.sin_port = htons(port_);////将整型变量从主机字节顺序转变成网络字节顺序,就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
struct linger optLinger = { 0 };//设置tcp连接断开方式,默认是优雅退出即全0
if(openLinger_) {
/* 优雅关闭: 直到所剩数据发送完毕或超时 */
optLinger.l_onoff = 1;
optLinger.l_linger = 1;//在close前延迟linger的时间,这段时间是优雅退出时间,超时则返回错误
}

listenFd_ = socket(AF_INET, SOCK_STREAM, 0);//开启一个连接,返回描述符,SOCK_STREAM基于TCP
if(listenFd_ < 0) {
LOG_ERROR("Create socket error!", port_);
return false;
}

ret = setsockopt(listenFd_, SOL_SOCKET, SO_LINGER, &optLinger, sizeof(optLinger));
if(ret < 0) {
close(listenFd_);
LOG_ERROR("Init linger error!", port_);
return false;
}

int optval = 1;
/* 端口复用 */
/* 只有最后一个套接字会正常接收数据。 */
//打开地址复用功能,允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接
//optval=true:如果在已经处于 ESTABLISHED状态下的socket调用closesocket(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket
//参考:https://blog.csdn.net/c_base_jin/article/details/94353956
//https://blog.csdn.net/u010144805/article/details/78579528
ret = setsockopt(listenFd_, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int));
if(ret == -1) {
LOG_ERROR("set socket setsockopt error !");
close(listenFd_);
return false;
}

ret = bind(listenFd_, (struct sockaddr *)&addr, sizeof(addr));//设置完就可以bind一个地址了,监听所有网卡
if(ret < 0) {
LOG_ERROR("Bind Port:%d error!", port_);
close(listenFd_);
return false;
}

//第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。
//socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
ret = listen(listenFd_, 6);
if(ret < 0) {
LOG_ERROR("Listen port:%d error!", port_);
close(listenFd_);
return false;
}
ret = epoller_->AddFd(listenFd_, listenEvent_ | EPOLLIN);//添加事件
if(ret == 0) {
LOG_ERROR("Add listen error!");
close(listenFd_);
return false;
}
/*
当 listenfd 设置成阻塞模式(默认行为,无需额外设置)时,如果连接 pending 队列中有需要处理的连接,accept 函数会立即返回,
否则会一直阻塞下去,直到有新的连接到来。
当 listenfd 设置成非阻塞模式,无论连接 pending 队列中是否有需要处理的连接,accept 都会立即返回,不会阻塞。
如果有连接,则 accept 返回一个大于 0 的值,这个返回值即是我们上文所说的 clientfd;如果没有连接,accept 返回值小于 0
*/
SetFdNonblock(listenFd_);//设置为非阻塞
LOG_INFO("Server port:%d", port_);
return true;
}

/*
阻塞方式是文件读写操作的默认方式,但是应用程序员可通过使用O_NONBLOCK 标志来人为
的设置读写操作为非阻塞方式 .( 该标志定义在 < linux/fcntl.h > 中,在打开文件时指定 ) .

如果设置了 O_NONBLOCK 标志,read 和 write 的行为是不同的 ,如果进程没有数据就绪时调用了 read ,
或者在缓冲区没有空间时调用了 write ,系统只是简单的返回 EAGAIN,而不会阻塞进程.
*/

//fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性
//F_GETFL:获取文件打开方式的标志,标志值含义与open调用一致,然后或上非阻塞标志
//F_SETFL:设置文件打开方式标志为arg指定方式
int WebServer::SetFdNonblock(int fd) {
assert(fd > 0);
return fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);//这里感觉是F_GETFL不是F_GETFD
}

main

config里啥东西没有,主要也没啥好配的,就直接main里面启动。服务器顶层的isclose没作用,ctrl+c终止进程,资源由操作系统自动回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* @Author : mark
* @Date : 2020-06-18
* @copyleft Apache 2.0
*/
#include <unistd.h>
#include "server/webserver.h"

int main() {
/* 守护进程 后台运行 */
//daemon(1, 0);

//服务器端口1316,监听和连接事件都是ET模式,连接1分钟无动作就关闭,linger全0是优雅退出
//mysql端口3306,用户名、密码、数据库名称
//连接池数量(同时维持连接的个数)、...
WebServer server(
1316, 3, 60000, false, /* 端口 ET模式 timeoutMs 优雅退出 */
3306, "root", "root", "webserver", /* Mysql配置 */
12, 6, true, 1, 1024); /* 连接池数量 线程池数量 日志开关 日志等级 日志异步队列容量 */
server.Start();
}

压力测试截图

image-20220925220717478

GitHub上的项目:qinguoyi/TinyWebServer: Linux下C++轻量级Web服务器学习 (github.com),这篇博客记录一下follow的日程和更详细的注解和逻辑思考。整个工程作者没有透露完成顺序,我就根据自己的理解从一个部分开始逐步往下。

第一站

lock

服务器需要一些互斥操作,因为一些共享资源(如数据库连接池、线程池)被同时访问时会出现错误,需要互斥访问。因此互斥作为一个小的辅助功能,在前面这里先进行分析。

locker.h代码如下:

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
#ifndef LOCKER_H
#define LOCKER_H

#include <exception>
#include <pthread.h>//for mutex
#include <semaphore.h>//for sem

class sem
{
public:
sem()
{
if (sem_init(&m_sem, 0, 0) != 0)//初始化不成功都返回异常
{
throw std::exception();
}
}
sem(int num)
{
if (sem_init(&m_sem, 0, num) != 0)//初始化不成功都返回异常
{
throw std::exception();
}
}
~sem()
{
sem_destroy(&m_sem);
}
bool wait()
{
return sem_wait(&m_sem) == 0;//阻塞等待资源,资源获取后往下执行
}
bool post()
{
return sem_post(&m_sem) == 0;//执行完毕,释放资源
}

private:
sem_t m_sem;//信号量对象
};
class locker
{
public:
locker()
{
if (pthread_mutex_init(&m_mutex, NULL) != 0)//初始化不成功都返回异常
{
throw std::exception();
}
}
~locker()
{
pthread_mutex_destroy(&m_mutex);
}
bool lock()
{
return pthread_mutex_lock(&m_mutex) == 0;
}
bool unlock()
{
return pthread_mutex_unlock(&m_mutex) == 0;
}
pthread_mutex_t *get()//取类私有成员
{
return &m_mutex;
}

private:
pthread_mutex_t m_mutex;//互斥锁对象,注意它本身不是指针,当参数时要用引用传入地址
};
class cond
{
public:
cond()
{
if (pthread_cond_init(&m_cond, NULL) != 0)//初始化不成功都返回异常
{
//pthread_mutex_destroy(&m_mutex);
throw std::exception();
}
}
~cond()
{
pthread_cond_destroy(&m_cond);
}
bool wait(pthread_mutex_t *m_mutex)
{
int ret = 0;
//pthread_mutex_lock(&m_mutex);
ret = pthread_cond_wait(&m_cond, m_mutex);
//pthread_mutex_unlock(&m_mutex);
return ret == 0;
}
bool timewait(pthread_mutex_t *m_mutex, struct timespec t)
{
int ret = 0;
//pthread_mutex_lock(&m_mutex);
ret = pthread_cond_timedwait(&m_cond, m_mutex, &t);
//pthread_mutex_unlock(&m_mutex);
return ret == 0;
}
bool signal()
{
return pthread_cond_signal(&m_cond) == 0;
}
bool broadcast()
{
return pthread_cond_broadcast(&m_cond) == 0;
}

private:
//static pthread_mutex_t m_mutex;
pthread_cond_t m_cond;//条件变量对象
};
#endif

互斥锁mutex

互斥锁主要是让一个资源锁起来,同一时间只能有一个活动在使用这个资源,其他的请求全部被卡住。项目中具体的实现不是用零散的mutex类的函数操作,而是用一个locker类封装好,构造和析构函数分别执行初始化和注销,这使得用户不需要手动去做(RAII思想,同时可以简化API较长的函数名,其他两个类也是这样的思想)。

  • int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
    • pthread_mutex_init() 函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如果参数attr为空(NULL),则使用默认的互斥锁属性,默认属性为快速互斥锁 。互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。
    • pthread_mutexattr_init() 函数成功完成之后会返回零,其他任何返回值都表示出现了错误。函数成功执行后,互斥锁被初始化为未锁住态。
  • *pthread_mutex_destroy()*用于注销一个互斥锁,API定义如下:int pthread_mutex_destroy(pthread_mutex_t *mutex*)*
    • 销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
  • int pthread_mutex_lock(pthread_mutex_t *mutex):锁住,返回值为0成功;
  • int pthread_mutex_unlock(pthread_mutex_t *mutex):解锁,返回值为0成功;

简单来说:

  • pthread_mutex_init函数用于初始化互斥锁
  • pthread_mutex_destory函数用于销毁互斥锁
  • pthread_mutex_lock函数以原子操作方式给互斥锁加锁
  • pthread_mutex_unlock函数以原子操作方式给互斥锁解锁

信号量sem

信号量有数值大小,主要用来管理一个buffer,写入和读出都要满足buffer的边界,同样的取用资源也要在资源池满足的情况下进行。

  • int sem_init(sem_t *sem, int pshared, unsigned int value);
    • 该函数初始化由 sem 指向的信号对象,并给它一个初始的整数值 value。pshared 控制信号量的类型,值为 0 代表该信号量用于多线程间的同步,值如果大于 0 表示可以共享,用于多个相关进程间的同步:参数 pshared > 0 时指定了 sem 处于共享内存区域,所以可以在进程间共享该变量
  • int sem_destroy(sem_t *sem);
    • 该函数用于对用完的信号量的清理。
  • int sem_wait(sem_t *sem);
    • sem_wait 是一个阻塞的函数,测试所指定信号量的值,它的操作是原子的。若 sem value > 0,则该信号量值减去 1 并立即返回。若sem value = 0,则阻塞直到 sem value > 0,此时立即减去 1,然后返回。函数成功返回0,错误的话信号量的值不改动,返回-1。
    • 还有另一个函数:sem_trywait 函数是非阻塞的函数,它会尝试获取获取 sem value 值,如果 sem value = 0,不是阻塞住,而是直接返回一个错误 EAGAIN。
  • int sem_post(sem_t *sem);
    • 把指定的信号量 sem 的值加 1,唤醒正在等待该信号量的任意线程。成功时返回 0;错误时,信号量的值没有更改,-1 被返回。

简单来说:

  • sem_init函数用于初始化一个未命名的信号量
  • sem_destory函数用于销毁信号量
  • sem_wait函数将以原子操作方式将信号量减一,信号量为0时,sem_wait阻塞
  • sem_post函数以原子操作方式将信号量加一,信号量大于0时,唤醒调用sem_post的线程

条件变量cond

  • int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);

    • 返回值:函数成功返回0;任何其他返回值都表示错误。初始化一个条件变量。当参数cattr为空指针时,函数创建的是一个缺省的条件变量。否则条件变量的属性将由cattr中的属性值来决定。调用 pthread_cond_init函数时,参数cattr为空指针等价于cattr中的属性为缺省属性,只是前者不需要cattr所占用的内存开销。这个函数返回时,条件变量被存放在参数cv指向的内存中。
  • int pthread_cond_destroy(pthread_cond_t *cv);

    • 返回值:函数成功返回0;任何其他返回值都表示错误。释放条件变量。需要注意的是只有在没有线程在该条件变量上等待时,才可以注销条件变量,否则会返回EBUSY。同时Linux在实现条件变量时,并没有为条件变量分配资源,所以在销毁一个条件变量时,只要注意该变量是否仍有等待线程即可。
  • int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex);

    • 返回值:函数成功返回0;任何其他返回值都表示错误。

    • 为什么要关联一个mutex呢?无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。

      • pthread_cond_wait(cond, mutex)的功能有3个:
        • 调用者线程首先释放mutex
        • 然后阻塞,等待被别的线程唤醒
        • 当调用者线程被唤醒后,调用者线程会再次获取mutex
      • pthread_cond_wait(cond)的功能只有1个:
        • 调用者线程阻塞,等待被别的线程唤醒。

      这里首先给一个简洁的回答:

      • 通常的应用场景下,当前线程执行pthread_cond_wait时,处于临界区访问共享资源,存在一个mutex与该临界区相关联,这是理解pthread_cond_wait带有mutex参数的关键
      • 当前线程执行pthread_cond_wait前,已经获得了和临界区相关联的mutex;因为缺少其他条件,执行pthread_cond_wait会阻塞,但是在进入阻塞状态前,必须释放已经获得的mutex,让其它线程能够进入临界区
      • 当前线程执行pthread_cond_wait后,阻塞等待的条件满足,条件满足时会被唤醒;被唤醒后,仍然处于临界区,因此被唤醒后必须再次获得和临界区相关联的mutex

      综上,调用pthread_cond_wait时,线程总是位于某个临界区,该临界区与mutex相关,pthread_cond_wait需要带有一个参数mutex,用于释放和再次获取mutex。

  • int pthread_cond_timedwait(pthread_cond_t *cv,pthread_mutex_t *mp, const struct timespec * abstime);

    • 返回值:函数成功返回0;任何其他返回值都表示错误
    • pthread_cond_timedwait()用于等待一个条件变量,等待条件变量的同时可以设置等待超时。这是一个非常有用的功能,如果不想一直等待某一条件变量,就可以使用这个函数。函数到了一定的时间,即使条件未发生也会解除阻塞。
    • 条件变量默认使用的时间是CLOCK_REALTIME。通过clock_gettime()接口获取时间。
  • int pthread_cond_signal(pthread_cond_t *cv);

    • 返回值:函数成功返回0;任何其他返回值都表示错误
    • 函数发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行。如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
  • int pthread_cond_broadcast(pthread_cond_t *cv);

    • 返回值:函数成功返回0;任何其他返回值都表示错误
    • 函数唤醒所有被pthread_cond_wait函数阻塞在某个条件变量上的线程,参数cv被用来指定这个条件变量。当没有线程阻塞在这个条件变量上时,pthread_cond_broadcast函数无效。

线程池threadpool

使用线程有动态方法和静态方法,动态方法是当一个工作需要完成时创建一个线程,当工作做完后释放线程。这种方式对资源的利用率高一些,但是耗费时间,因为要新创建、销毁线程。静态方法是使用线程池先创建好一系列等待请求的线程,当一个工作到来时直接分配空闲线程,工作完成后放回线程池。

线程池的代码放在threadpool.h中,代码如下:

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
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include <list>
#include <cstdio>
#include <exception>
#include <pthread.h>
#include "../lock/locker.h"
#include "../CGImysql/sql_connection_pool.h"

template <typename T>
class threadpool
{
public:
/*thread_number是线程池中线程的数量,max_requests是请求队列中最多允许的、等待处理的请求的数量*/
threadpool(int actor_model, connection_pool *connPool, int thread_number = 8, int max_request = 10000);
~threadpool();
bool append(T *request, int state);//两种append,应该对应了不同的T的操作,实际上感觉重载就可以了
bool append_p(T *request);

private:
/*工作线程运行的函数,它不断从工作队列中取出任务并执行之*/
static void *worker(void *arg);//静态成员函数,是一个回调函数,后面会说明为什么要设置成静态
void run();

private:
int m_thread_number; //线程池中的线程数,即可同时工作的数量
int m_max_requests; //请求队列中允许的最大请求数,即最多同时等待的数量
pthread_t *m_threads; //描述线程池的数组,其大小为m_thread_number
std::list<T *> m_workqueue; //请求队列
locker m_queuelocker; //保护请求队列的互斥锁
sem m_queuestat; //是否有任务需要处理
connection_pool *m_connPool; //数据库
int m_actor_model; //模型切换
};
template <typename T>
threadpool<T>::threadpool( int actor_model, connection_pool *connPool, int thread_number, int max_requests) : m_actor_model(actor_model),m_thread_number(thread_number), m_max_requests(max_requests), m_threads(NULL),m_connPool(connPool)
{
if (thread_number <= 0 || max_requests <= 0) //一些不合理请求的判断
throw std::exception();
m_threads = new pthread_t[m_thread_number]; //线程池的实作是一个线程数组
if (!m_threads) //请求失败则m_threads是一个nullptr
throw std::exception();
for (int i = 0; i < thread_number; ++i)
{ //初始化线程池里的线程,返回值不为0说明失败
if (pthread_create(m_threads + i, NULL, worker, this) != 0)//m_threads+i与m_threads[i]没区别
{
delete[] m_threads;
throw std::exception();
}
//在创建线程后,实现线程从主线程(进程)分离,这使得线程能在工作完后自动回收资源,具体在后面有写
if (pthread_detach(m_threads[i]))//感觉这个if和上面那个if的风格好不一样...
{
delete[] m_threads;
throw std::exception();
}
}
}
template <typename T>
threadpool<T>::~threadpool()//析构,new出来的delete掉
{
delete[] m_threads;
}
//向请求队列添加请求
template <typename T>
bool threadpool<T>::append(T *request, int state)
{
m_queuelocker.lock();//多线程状态下工作,要互斥,否则request和list的修改会出现异常
if (m_workqueue.size() >= m_max_requests)//超出最大请求,非阻塞返回。如果要阻塞的话,可以用一个full信号量控制
{
m_queuelocker.unlock();
return false;
}
request->m_state = state;//赋予状态,指读还是写
m_workqueue.push_back(request);//添加队列
m_queuelocker.unlock();//解锁
m_queuestat.post();//信号量加一,告知线程池有任务在等待处理
return true;
}
//和上面一样
template <typename T>
bool threadpool<T>::append_p(T *request)
{
m_queuelocker.lock();
if (m_workqueue.size() >= m_max_requests)
{
m_queuelocker.unlock();
return false;
}
m_workqueue.push_back(request);
m_queuelocker.unlock();
m_queuestat.post();
return true;
}
//worker函数,传递参数给线程,然后调用真正的run函数工作。静态成员类外定义不用static
template <typename T>
void *threadpool<T>::worker(void *arg)//void 指针可以指向任意类型的数据
{
threadpool *pool = (threadpool *)arg;//arg = this,将参数强转为线程池类,调用成员方法
pool->run();//调用实例的run函数
return pool;//实际上run一直运行,估计不会return
}
template <typename T>
void threadpool<T>::run()
{
while (true)//为什么是while呢?因为每个线程其实在不断的运行,如果有任务就取出来做,没有就wait阻塞
{
m_queuestat.wait();//阻塞,要等有任务即前面post信号量了,才往下做
m_queuelocker.lock();//取出任务,要对list操作,那么要锁
if (m_workqueue.empty())//感觉没必要,前面用wait其实已经判断了工作池buffer了,有任务才会往下
{//m_workqueue的大小应该和m_queuestat信号量的大小绑定了(根据append函数来看)
m_queuelocker.unlock();
continue;
}
T *request = m_workqueue.front();//取第一个请求
m_workqueue.pop_front();//pop
m_queuelocker.unlock();//解锁,让下一个线程可以操作list
if (!request)//如果请求实际上是null
continue;
//切换模式,reactor==1,proactor==0
//非阻塞同步工作模式,读写均需要在线程里工作,调用read和write,并且进行最后的process业务逻辑处理。
//非阻塞是指有数据才进行,但数据处理过程仍在线程里执行
if (1 == m_actor_model)
{
if (0 == request->m_state)//读
{
if (request->read_once())//如果成功则不关闭定时器,不用关闭连接
{
request->improv = 1;
connectionRAII mysqlcon(&request->mysql, m_connPool);
request->process();
}
else//不成功要关闭连接和定时器
{
request->improv = 1;
request->timer_flag = 1;
}
}
else//写
{
if (request->write())
{
request->improv = 1;
}
else
{
request->improv = 1;
request->timer_flag = 1;
}
}
}
else//模拟proactor模式的IO在主循环处理(是同步的),线程只需要处理业务逻辑即可
{
connectionRAII mysqlcon(&request->mysql, m_connPool);
request->process();//处理业务逻辑
}
}
}
#endif
  • 线程池类使用模板,目前还没看出作用,猜测是后面会用于多种不同资源的分配使用,如处理http连接、处理数据库请求等等。
  • int pthread_create(pthread_t *tidp, const pthread_attr_t *attr,( void *)(*start_rtn)( void *), void *arg);
    • 第一个参数为指向线程 标识符的指针。
    • 第二个参数用来设置线程属性。
    • 第三个参数是线程运行函数的起始地址(函数指针)。
    • 最后一个参数是运行函数的参数。
    • 若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。
  • int pthread_detach(pthread_t thread); 成功:0;失败:错误号
    • 使用pthread_create创建的线程有两种状态:joinable和unjoinable。默认是joinable 状态。
    • 线程创建后在线程中调用 pthread_detach, 如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。
    • pthread_detach()和pthread_join()就是控制子线程回收资源的两种不同的方式。同一进程间的线程具有共享和独立的资源,其中共享的资源有堆、全局变量、静态变量、文件等公用资源。而独享的资源有栈和寄存器,这两种方式就是决定子线程结束时如何回收独享的资源。
      • 如果是joinable状态,则该线程结束后(通过pthread_exit结束或者线程执行体任务执行完毕)不会释放线程所占用堆栈和线程描述符(总计8K多)等资源,除非在主线程调用了pthread_join函数之后才会释放。pthread_join函数一般应用在主线程需要等待子线程结束后才继续执行的场景。(pthread_join是一个阻塞函数,调用方会阻塞到pthread_join所指定的tid的线程结束后才被回收,但是在此之前,调用方是霸占系统资源的。 )
      • 如果是unjoinable状态,则该线程结束后会自动释放占用资源。实现方式是在创建时指定属性,或者在线程执行体的最开始处添加一行:pthread_detach(pthread_self());不会阻塞,调用它后,线程运行结束后会自动释放资源,后者非常方便。
    • 总结
      • pthread_detach()即主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收。
      • pthread_join()即是子线程合入主线程,主线程会一直阻塞,直到子线程执行结束,然后回收子线程资源,并继续执行。
  • 工作流程
    • 构造函数初始化线程池:创建线程和分离线程状态
    • 析构函数销毁线程池
    • append函数互斥地向list添加请求,并post信号量
    • 一个线程对应一个worker,worker函数调用run。
    • run函数从list互斥地获得请求并工作,不断循环
  • worker函数是一个成员函数,那么必须是一个静态的。它是一个回调函数,回调函数是通过指针调用的函数,最常使用的回调函数就是在创建线程时(pthread_create),以一个函数指针以及传递给这个函数多个参数来调用线程函数来创建线程。那么一般的类成员函数是不能用作回调函数的,因为在使用回调函数时,会传递指定的符合回调函数声明的的参数给回调函数,而类成员函数隐式包含一个this指针参数,所以把类成员函数当作回调函数编译时会因为参数不匹配会出错(回调后多了个this,与声明不一致)。
    • 静态成员函数就没有这个问题,里面没有this指针。
  • 那么为什么要用worker间接调用run函数呢?run设计成静态的直接调用不行吗?
    • 答案是不太方便,因为静态成员函数只能访问静态成员数据、其他静态成员和类外部的函数,因为没有this指针。不过我们这里手动传入了this指针使得它可以调用run成员函数。
    • this指针只能在类内部使用而不能在外部使用。可以访问类中所有public、private、protect的成员函数和变量。this指针是指向对象的实例,所以只有当对象被创建时this指针才有效。
    • 同一个模板类的不同实例共享静态成员函数,不同实例有不同的资源,这导致静态成员函数不能访问那些实例各有的资源,因为不知道要访问哪个。而run要操作不同实例的list等等资源,通过共享的worker使用从线程传入的this指针操作各个实例的run函数,run就能操作自己这个实例的资源了。但如果run是静态的,即使通过手动传入this参数,run里面所有的资源都要this->一下,太不方便了。
    • 因此,最好的方式就是静态成员函数通过this指针调用成员函数,这个成员函数就可以很方便地访问类实例的资源了(说白了就是使用资源不用this->了)。

第二站

单例模式

后面会用到单例模式,这里先详解一下,参考了许多文章。

什么是单例模式?

保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。

为什么要用单例模式?

1、单例模式节省公共资源

比如:大家都要喝水,但是没必要每人家里都打一口井是吧,通常的做法是整个村里打一个井就够了,大家都从这个井里面打水喝。

对应到我们计算机里面,像日志管理、打印机、数据库连接池、应用配置。

2、单例模式方便控制

就像日志管理,如果多个人同时来写日志,你一笔我一笔那整个日志文件都乱七八糟,如果想要控制日志的正确性,那么必须要对关键的代码进行上锁,只能一个一个按照顺序来写,而单例模式只有一个人来向日志里写入信息方便控制,避免了这种多人干扰的问题出现。

实现单例模式的思路

1. 构造私有:

如果要保证一个类不能多次被实例化,那么我肯定要阻止对象被new 出来,所以需要把类的所有构造方法私有化

2.以静态方法返回实例

因为外界就不能通过new来获得对象,所以我们要通过提供类的方法来让外界获取对象实例。

3.确保对象实例只有一个

只对类进行一次实例化,以后都直接获取第一次实例化的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 单例模式案例
*/
public class Singleton {
//确保对象实例只有一个。
private static final Singleton singleton = new Singleton();
//构造方法私有
private Singleton() {
}
//以静态方法返回实例
public static Singleton getInstance() {
return singleton;
}
}

这里类的实例在类初始化的时候已经生成,不再进行第二次实例化了,而外界只能通过SingleCase.getInstance()方法来获取SingleCase对象, 所以这样就保证整个系统只能获取一个类的对象实例。

单例模式的两种实现模式

饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。

1
2
优点:简单
缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Singleton
{
public:
static Singleton* GetInstance()
{
return &m_instance;
}
private:
// 构造函数私有
Singleton(){};
// C++98 防拷贝
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
// or
// C++11
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton m_instance;
};
Singleton Singleton::m_instance; // 在程序入口之前就完成单例对象的初始化

如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。


懒汉模式:等到用的的时候程序再创建实例对象

1
2
优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制。
缺点:复杂
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
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
class Singleton
{
public:
static Singleton* GetInstance() {
// 注意多线程环境下一定要使用Double-Check的方式加锁,才能保证效率和线程安全
if (nullptr == m_pInstance) {
m_mtx.lock();
if (nullptr == m_pInstance) {
m_pInstance = new Singleton();
}
m_mtx.unlock();
}
return m_pInstance;
}
// 实现一个内嵌垃圾回收类
class CGarbo {
public:
~CGarbo(){
if (Singleton::m_pInstance)
delete Singleton::m_pInstance;
}
};
// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
static CGarbo Garbo;
private:
// 构造函数私有
Singleton(){};
// 防拷贝
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
static Singleton* m_pInstance; // 单例对象指针
static mutex m_mtx; //互斥锁
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_mtx;

添加一个类的静态对象,总是让人不太满意,所以有人用如下方法来重新实现单例和解决它相应的问题,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
CSingleton(const CSingleton &);
CSingleton & operator = (const CSingleton &);
public:
static CSingleton * GetInstance()
{
static CSingleton instance; //局部静态变量,在这个局部静态函数销毁才销毁,也就是当程序结束才销毁
return &instance;//不管怎么getinstance,都只定义一次instance,返回的都是同一个实例
}
};

使用局部静态变量是非常强大的方法,完全实现了单例的特性,而且代码量更少,也不用担心单例销毁的问题。

sql数据库连接

头文件

数据库连接的头文件声明了很多信息,我们先分析头文件的逻辑,再去看定义的实现

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
#ifndef _CONNECTION_POOL_
#define _CONNECTION_POOL_

#include <stdio.h>
#include <list>
#include <mysql/mysql.h>
#include <error.h>
#include <string.h>
#include <iostream>
#include <string>
#include "../lock/locker.h"
#include "../log/log.h"

using namespace std;

class connection_pool
{
public:
MYSQL *GetConnection(); //获取数据库连接
bool ReleaseConnection(MYSQL *conn); //释放连接
int GetFreeConn(); //获取空闲连接数目
void DestroyPool(); //销毁所有连接

//单例模式
static connection_pool *GetInstance();

void init(string url, string User, string PassWord, string DataBaseName, int Port, int MaxConn, int close_log);

private:
connection_pool();
~connection_pool();

int m_MaxConn; //最大连接数
int m_CurConn; //当前已使用的连接数
int m_FreeConn; //当前空闲的连接数
locker lock;//互斥锁
list<MYSQL *> connList; //连接池
sem reserve;//信号量

public:
string m_url; //主机地址
string m_Port; //数据库端口号
string m_User; //登陆数据库用户名
string m_PassWord; //登陆数据库密码
string m_DatabaseName; //使用数据库名
int m_close_log; //日志开关
};

class connectionRAII{

public:
//双指针对MYSQL *con修改。数据库连接本身是指针类型,所以参数需要通过双指针才能对其进行修改。
connectionRAII(MYSQL **con, connection_pool *connPool);
~connectionRAII();

private:
MYSQL *conRAII;//这个RAII类拥有一个MYSQL连接
connection_pool *poolRAII;//且要有一个连接池指针指向那个单例对象,调用释放函数把MYSQL连接释放
};

#endif

头文件里主要是对connection_pool这个类的功能进行了声明:

  • 四种主要功能:获取数据库连接、获取空闲连接数目、释放连接、销毁所有连接。四个功能函数实际上从返回值就可以看出区别和要做什么事。
  • 数据库初始化init函数,它通过地址、端口、用户名密码、使用的数据库名称来进行数据库的连接。
  • 单例模式,把构造函数放private,使得只能用静态成员函数在类中创建类对象;把析构函数放private,使得无法在外部delete类对象,只能用内部的成员函数delete this,因为内部成员函数才能访问私有的析构函数。不过这里的单例模式不用new,因此也就没有对应的delete函数。

还有一个connectionRAII类,这个类对连接池对象进行RAII式的管理,前面可以看到有个释放连接的功能,我们不想手动释放,就可以在这个类的析构函数里释放,具体看实现就好了。

  • 为什么con是双指针,可以参考c/c++向函数传递指针并修改其指向的问题_AlanChaw292的博客-CSDN博客_c++改变指针指向。大概的意思就是,如果是单指针传进来,编译器也会为形参做一个备份,如传入一个p,会备份一个p1(我们实际上使用的是p1,跟值传递是一个意思),p和p1的值相同,都指向对象的地址。我们当然可以使用p1来修改指向的值,但无法通过修改p1修改p(就像形参无法影响实参),也就是说传入的指针不能修改指针本身的地址(不能修改指针的指向,不是不允许,而是没意义)。这种时候,就需要用双指针,指向我们想修改的指针的地址,这样就行了,那篇博客讲的很清楚。

.cpp实现

话不多说,先上源码

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
#include <mysql/mysql.h>
#include <stdio.h>
#include <string>
#include <string.h>
#include <stdlib.h>
#include <list>
#include <pthread.h>
#include <iostream>
#include "sql_connection_pool.h"

using namespace std;

connection_pool::connection_pool()
{ //类内成员初始化
m_CurConn = 0;//已使用的是0
m_FreeConn = 0;//空闲的还不知道,但是没有init时就是0
}

connection_pool *connection_pool::GetInstance()//静态成员函数,单例模式
{
static connection_pool connPool;//创建静态的连接池对象,只定义一次,每次调用都返回它
return &connPool;//且是通过指针(地址)返回,不会导致拷贝构造
//这个静态对象销毁是在静态成员函数销毁时销毁,而这个函数在程序结束才销毁...
}

//构造初始化
void connection_pool::init(string url, string User, string PassWord, string DBName, int Port, int MaxConn, int close_log)
{
//给类成员赋值,这些类成员是为了以后访问连接池对象可以获取信息

m_url = url;
m_Port = Port;
m_User = User;
m_PassWord = PassWord;
m_DatabaseName = DBName;
m_close_log = close_log;
m_MaxConn = m_FreeConn;

for (int i = 0; i < MaxConn; i++)//一共(最多)可以有maxconn个连接
{
MYSQL *con = NULL;
con = mysql_init(con);//分配并初始化一个新对象

if (con == NULL)//NULL说明没有足够的内存分配
{
LOG_ERROR("MySQL Error");
exit(1);
}
//前面的init初始化了一个mysql的数据结构,现在real connect进行真正的连接
con = mysql_real_connect(con, url.c_str(), User.c_str(), PassWord.c_str(), DBName.c_str(), Port, NULL, 0);

if (con == NULL)//连接失败返回NULL
{
LOG_ERROR("MySQL Error");
exit(1);
}
connList.push_back(con);//成功则在连接池(list)里添加
++m_FreeConn;//空闲连接+1
}

reserve = sem(m_FreeConn);//给这个信号量赋值,实际上可以在for循环里post,不过逻辑有点怪就是了
}


//当有请求时,从数据库连接池中返回一个可用连接,更新使用和空闲连接数
MYSQL *connection_pool::GetConnection()
{
MYSQL *con = NULL;//创建一个指针,将要指向连接池已经创建的连接

if (0 == connList.size())//没有就没有了,不阻塞
return NULL;

reserve.wait();//有的话就让信号量减一,不过既然前面return了,不阻塞了还有信号量干啥嘞

lock.lock();//互斥访问这个连接,修改连接池(连接池是共享的),以及互斥修改一些表示buffer大小数据

con = connList.front();
connList.pop_front();

--m_FreeConn;//连接池容量buffer-1
++m_CurConn;//连接池buffer使用+1

lock.unlock();//解锁
return con;
}

//释放当前使用的连接
bool connection_pool::ReleaseConnection(MYSQL *con)
{
if (NULL == con)
return false;

lock.lock();//回收连接,放回连接池,既然访问连接池这个公共资源,要互斥锁住

connList.push_back(con);//放回
++m_FreeConn;
--m_CurConn;

lock.unlock();

reserve.post();//信号量+1,越发感觉信号量和freeconn是一个东西?以及connlist.size()...
return true;
}

//销毁数据库连接池
void connection_pool::DestroyPool()
{

lock.lock();//主线程要关闭连接池,要等连接池操作完再说,不然在销毁过程中可能又同时放回,会混乱
//且其他线程在获取连接时,也要等连接池销毁的操作,不然连接池都销毁了还拿到了一个连接
if (connList.size() > 0)
{
list<MYSQL *>::iterator it;
for (it = connList.begin(); it != connList.end(); ++it)
{
MYSQL *con = *it;
mysql_close(con);//一个一个关闭掉,但mysql对象、结构还在list里
}
m_CurConn = 0;//这些遍历的修改也要在临界区嘛
m_FreeConn = 0;
connList.clear();//移除所有元素,把那些关闭了的连接都删掉
}

lock.unlock();
}

//当前空闲的连接数
int connection_pool::GetFreeConn()
{
return this->m_FreeConn;//这个就不锁了,没什么意义,就放回“当下”的值就好了。
}

connection_pool::~connection_pool()
{
DestroyPool();//析构连接池
}


//这个RAII类是针对单个sql连接的,具体怎么使用还要看实际代码,
//注意这个双指针修改sql连接本身的值(指向连接的地址),使这个连接可以更改指向,(可能从null)指向连接池的可用的连接。
connectionRAII::connectionRAII(MYSQL **SQL, connection_pool *connPool){
*SQL = connPool->GetConnection();

conRAII = *SQL;//这个RAII类本身也要存一个备份,使得调用析构函数释放连接时知道要释放的连接的地址
poolRAII = connPool;
}

connectionRAII::~connectionRAII(){
poolRAII->ReleaseConnection(conRAII);//析构函数:释放连接
}
  • MYSQL *mysql_init(MYSQL *mysql)
    • 分配或初始化与mysql_real_connect()相适应的MYSQL对象。如果mysql是NULL指针,该函数将分配、初始化、并返回新对象。否则,将初始化对象,并返回对象的地址。如果mysql_init()分配了新的对象,当调用mysql_close()来关闭连接时。将释放该对象。
    • 返回值:初始化的MYSQL*句柄。如果无足够内存以分配新的对象,返回NULL。
  • MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag)
    • mysql_real_connect()尝试与运行在主机上的MySQL数据库引擎建立连接。在你能够执行需要有效MySQL连接句柄结构的任何其他API函数之前,mysql_real_connect()必须成功完成。
    • 参数:
      • 第1个参数应是已有MYSQL结构的地址。调用mysql_real_connect()之前,必须调用mysql_init()来初始化MYSQL结构。通过mysql_options()调用,可更改多种连接选项。
      • “host”的值必须是主机名或IP地址。如果“host”是NULL或字符串”localhost”,连接将被视为与本地主机的连接。如果操作系统支持套接字(Unix)或命名管道(Windows),将使用它们而不是TCP/IP连接到服务器。
      • “user”参数包含用户的MySQL登录ID。如果“user”是NULL或空字符串””,用户将被视为当前用户。在UNIX环境下,它是当前的登录名。在Windows ODBC下,必须明确指定当前用户名。
      • “passwd”参数包含用户的密码。如果“passwd”是NULL,仅会对该用户的(拥有1个空密码字段的)用户表中的条目进行匹配检查。这样,数据库管理员就能按特定的方式设置MySQL权限系统,根据用户是否拥有指定的密码,用户将获得不同的权限。
      • “db”是数据库名称。如果db为NULL,连接会将默认的数据库设为该值。
      • 如果“port”不是0,其值将用作TCP/IP连接的端口号。注意,“host”参数决定了连接的类型。
      • 如果unix_socket不是NULL,该字符串描述了应使用的套接字或命名管道。注意,“host”参数决定了连接的类型。
      • client_flag的值通常为0,其他标志可以实现特定的功能
    • 返回值:如果连接成功,返回MYSQL*连接句柄。如果连接失败,返回NULL。对于成功的连接,返回值与第1个参数的值相同。
  • void mysql_close(MYSQL *mysql)
    • 关闭前面打开的连接。如果句柄是由mysql_init()或mysql_connect()自动分配的,mysql_close()还将解除分配由mysql指向的连接句柄。
  • string.c_str():
    • const char *c_str();
    • c_str()函数返回一个指向正规C字符串的指针常量, 内容与本string串相同。
    • 这是为了与c语言兼容,在c语言中没有string类型,故必须通过string类对象的成员函数c_str()把string 对象转换成c中的字符串样式。

第三站

日志系统

这里可以看看作者的讲解先理解一下:

最新版Web服务器项目详解 - 09 日志系统(上) (qq.com)

最新版Web服务器项目详解 - 10 日志系统(下) (qq.com)

阻塞队列

写入日志有同步的写入和异步的写入方式,同步的方式是当产生日志时就写入,主线程工作推迟;异步的写入方式是使用一个日志线程来管理,“写入日志”这个任务就需要有地方放,因此就要用一个阻塞队列来存放任务。为什么是阻塞的呢,日志线程有多个吗?实际上日志线程只有一个,但其他线程可以有多个,这就是一个多生产者–单消费者的模型。因此常规的解法就是用互斥锁+buffer信号量的组合。但这个阻塞队列还添加了超时处理的功能,信号量就需要改成条件变量,条件变量我没怎么使用过,之后回过头再整理一下,不过具体的功能在lock那章中写了。

下面是阻塞队列的代码,在头文件block_queue.h中

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
/*************************************************************
*循环数组实现的阻塞队列,m_back = (m_back + 1) % m_max_size;
*线程安全,每个操作前都要先加互斥锁,操作完后,再解锁
**************************************************************/

#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H

#include <iostream>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>
#include "../lock/locker.h"
using namespace std;

template <class T>
class block_queue
{
public:
block_queue(int max_size = 1000)
{
if (max_size <= 0)
{
exit(-1);
}
//初始化
m_max_size = max_size;
m_array = new T[max_size];
m_size = 0;
m_front = -1;
m_back = -1;
}
//剩下的操作,涉及对队列内部元素的操作(插入删除)、对队列变量的访问(size,头尾指针等),都需要互斥访问
void clear()
{
m_mutex.lock();
m_size = 0;
m_front = -1;
m_back = -1;
m_mutex.unlock();
}

~block_queue()
{
m_mutex.lock();
if (m_array != NULL)//少见...不过健壮(也许多余?)
delete [] m_array;

m_mutex.unlock();
}
//判断队列是否满了
bool full()
{
m_mutex.lock();//访问msize,要锁
if (m_size >= m_max_size)
{

m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//判断队列是否为空
bool empty()
{
m_mutex.lock();//访问msize,要锁
if (0 == m_size)
{
m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//返回队首元素
bool front(T &value)//以参数形式返回
{
m_mutex.lock();
if (0 == m_size)
{
m_mutex.unlock();
return false;
}
value = m_array[m_front];
m_mutex.unlock();
return true;
}
//返回队尾元素
bool back(T &value)
{
m_mutex.lock();
if (0 == m_size)
{
m_mutex.unlock();
return false;
}
value = m_array[m_back];
m_mutex.unlock();
return true;
}

int size()
{
int tmp = 0;//不直接return,因为要加锁,return不能放锁里

m_mutex.lock();
tmp = m_size;

m_mutex.unlock();
return tmp;
}

int max_size()
{
int tmp = 0;

m_mutex.lock();
tmp = m_max_size;

m_mutex.unlock();
return tmp;
}


//往队列添加元素,需要将所有使用队列的线程先唤醒,这些线程除了等待锁,还要等待任务出现以pop,因此push要唤醒它们
//在应用上,调用pop的就一个日志线程

//当有元素push进队列,相当于生产者生产了一个元素
//若当前没有线程等待条件变量,则唤醒无意义
bool push(const T &item)
{

m_mutex.lock();
if (m_size >= m_max_size)//队列满了,赶紧让pop线程做事
{

m_cond.broadcast();//唤醒所有在wait的线程
//在wait说明之前队列空了,但怎么会从空->满呢?可能是一直被push抢了互斥锁
//因此这个唤醒让那些卡在while的pop从wait解放,然后一个一个等待抢占互斥锁做事(和pop抢也和push抢)
m_mutex.unlock();
return false;
}

m_back = (m_back + 1) % m_max_size;
m_array[m_back] = item;

m_size++;

m_cond.broadcast();
m_mutex.unlock();
return true;
}
//pop时,如果当前队列没有元素,将会等待条件变量
bool pop(T &item)
{

m_mutex.lock();//条件变量在临界区用,wait自身会解锁-等待唤醒-抢占锁
//pop被多个线程调用,前面push都唤醒了那么这里会竞争任务,可能只有一部分线程执行了这个m_size就=0了
//那么此时就要继续等待,因此用while而不是用if,if只能wait一次(这种情况是虚假唤醒)
while (m_size <= 0)
{

if (!m_cond.wait(m_mutex.get()))//等待唤醒
{
m_mutex.unlock();
return false;//wait出错就return
}
}
//条件变量被唤醒,抢到了互斥锁,且whlie正常退出,开始做事
m_front = (m_front + 1) % m_max_size;
item = m_array[m_front];
m_size--;
m_mutex.unlock();
return true;
}

//增加了超时处理
bool pop(T &item, int ms_timeout)//可以超时多少毫秒
{
//时间类下面介绍
struct timespec t = {0, 0};//一个秒,一个纳秒
struct timeval now = {0, 0};//一个秒,一个微秒
gettimeofday(&now, NULL);//获取系统当前时间
m_mutex.lock();
//如果要等待的话,就进去wait,注意这里不是一直等到可以调用,所以不用while
//如果超时就到下面的if返回,如果没超时就被唤醒,那么会有虚假唤醒的情况,
//因此下面还要if判断一下,虚假唤醒就直接返回,所以这里超时就不做、虚假唤醒也不做
if (m_size <= 0)
{
//t是前面获取的时间加上超时的时间
t.tv_sec = now.tv_sec + ms_timeout / 1000;//取秒位
t.tv_nsec = (ms_timeout % 1000) * 1000;//剩下没取到的毫秒(余数)弄成纳秒(为什么是*1000)
//整体时间计算是秒+纳秒
if (!m_cond.timewait(m_mutex.get(), t))//时间到了就不等待唤醒了,直接润
{
m_mutex.unlock();
return false;
}
}
//上面润完就到这里,注意因为抢了锁,所以不可能有push,这里一定是返回的
if (m_size <= 0)
{
m_mutex.unlock();
return false;
}
//正常干活
m_front = (m_front + 1) % m_max_size;
item = m_array[m_front];
m_size--;
m_mutex.unlock();
return true;
}

private:
locker m_mutex;//互斥锁
cond m_cond;//条件变量

T *m_array;//队列空间
int m_size;//队列目前大小
int m_max_size;//队列大小,使用这个变量就无需额外留一个空间
int m_front;//队列头部
int m_back;//队列尾部
};

#endif

在C语言中可以使用函数gettimeofday()函数来得到精确时间。它的精度可以达到微妙,是C标准库的函数。

在gettimeofday()函数中tv或者tz都可以为空。如果为空则就不返回其对应的结构体。

函数执行成功后返回0,失败后返回-1,错误代码存于errno中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<sys/time.h>

int gettimeofday(struct timeval*tv,struct timezone *tz )

struct timeval{

long tv_sec; /*秒*/

long tv_usec; /*微妙*/

};

struct timezone{

int tz_minuteswest;/*和greenwich 时间差了多少分钟*/

int tz_dsttime; /*type of DST correction*/

}

说明:在使用gettimeofday()函数时,第二个参数一般都为空,因为我们一般都只是为了获得当前时间,而不用获得timezone的数值。

头文件

定义了Log类,其中使用宏来为其他程序提供接口。

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
#ifndef LOG_H
#define LOG_H

#include <stdio.h>
#include <iostream>
#include <string>
#include <stdarg.h>//与宏有关
#include <pthread.h>
#include "block_queue.h"

using namespace std;

class Log
{
public:
//C++11以后,使用局部变量懒汉不用加锁
static Log *get_instance()
{
static Log instance;
return &instance;
}

static void *flush_log_thread(void *args)//是一个worker函数
{
Log::get_instance()->async_write_log();//静态成员函数的调用:A::func()
}
//可选择的参数有日志文件、日志缓冲区大小、最大行数以及最长日志条队列
bool init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);

void write_log(int level, const char *format, ...);

void flush(void);

private:
Log();
virtual ~Log();
void *async_write_log()//write_log执行push任务的功能,这个函数只取任务写到文件
{
string single_log;
//从阻塞队列中取出一个日志string,写入文件
while (m_log_queue->pop(single_log))//取是互斥的,写也是互斥的,但是两个锁并不相同
{
m_mutex.lock();//写入m_fp中,共享的文件空间的要锁一下
fputs(single_log.c_str(), m_fp);
m_mutex.unlock();
}
}

private:
char dir_name[128]; //路径名
char log_name[128]; //log文件名
int m_split_lines; //日志最大行数
int m_log_buf_size; //日志缓冲区大小
long long m_count; //日志行数记录
int m_today; //因为按天分类,记录当前时间是那一天
FILE *m_fp; //打开log的文件指针
char *m_buf;
block_queue<string> *m_log_queue; //阻塞队列
bool m_is_async; //是否同步标志位
locker m_mutex;
int m_close_log; //关闭日志
};
//宏接口,调用write_log和flush
#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}

#endif

.cpp实现

主要是一些string的操作,因为写日志就是把字符写入文件嘛

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
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <stdarg.h>
#include "log.h"
#include <pthread.h>
using namespace std;

Log::Log()
{
m_count = 0;//每次行数重置为0,包括天数的记录也会重置,所以如果关闭了的话前面的记录就不存在,重复写一个日志文件就可能出错,因此如果关掉程序再打开的话,最好换一个文件重新开始写
m_is_async = false;
}

Log::~Log()
{
if (m_fp != NULL)
{
fclose(m_fp);
}
}
//异步需要设置阻塞队列的长度,同步不需要设置
bool Log::init(const char *file_name, int close_log, int log_buf_size, int split_lines, int max_queue_size)
{
//如果设置了max_queue_size,则设置为异步,只有一个线程在取任务处理
if (max_queue_size >= 1)
{
m_is_async = true;//设置写入方式flag
m_log_queue = new block_queue<string>(max_queue_size);//创建并设置阻塞队列长度
pthread_t tid;
//flush_log_thread为回调函数,这里表示创建线程异步写日志
pthread_create(&tid, NULL, flush_log_thread, NULL);
}
//成员初始化
m_close_log = close_log;//1的话关闭日志功能
m_log_buf_size = log_buf_size;//缓冲区大小
m_buf = new char[m_log_buf_size];//缓冲区
memset(m_buf, '\0', m_log_buf_size);//缓冲区数值初始化
m_split_lines = split_lines;//最大行数

//见后面,实际上就是得到具体的本地的时间,年月日时分秒等等
time_t t = time(NULL);
struct tm *sys_tm = localtime(&t);
struct tm my_tm = *sys_tm;

//从后往前找到第一个/的位置
const char *p = strrchr(file_name, '/');//该函数见后面
char log_full_name[256] = {0};//接下来要生成一个具体的日志文件名


//接下来相当于自定义日志名
//若输入的文件名没有/,则直接将时间+文件名作为日志名
if (p == NULL)
{
snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);//该函数见后面
//下面两行是我自己分析觉得要加上的地方,否则创建新日志的名字可能不协同
dir_name = "";
log_name = file_name;
}
//如果有的话,就是一个路径了,就要从/后面开始添加时间
else
{
//将/的位置向后移动一个位置,然后复制到logname中
//p - file_name + 1是文件所在路径文件夹的长度
strcpy(log_name, p + 1);//存一下log_name
strncpy(dir_name, file_name, p - file_name + 1);
snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);//dirname相当于./,这里就比上面多一个路径名,不过把filename拆分为dirname和logname,补个时间
}

m_today = my_tm.tm_mday;//更新日期

m_fp = fopen(log_full_name, "a");//根据上面的一系列操作获得的名称打开文件或创建文件
//a表示追加到一个文件。写操作向文件末尾追加数据。如果文件不存在,则创建文件。
if (m_fp == NULL)
{
return false;
}

return true;
}


void Log::write_log(int level, const char *format, ...)//可变参数
{
//获取具体时间
struct timeval now = {0, 0};
gettimeofday(&now, NULL);//返回当前距离1970年的秒数和微妙数
time_t t = now.tv_sec;//取得从1970年1月1日至今的秒数。
struct tm *sys_tm = localtime(&t);//将time_t表示的时间转换为经过时区转换的UTC时间
struct tm my_tm = *sys_tm;

char s[16] = {0};//日志类型标头
switch (level)//日志分级
{
case 0:
strcpy(s, "[debug]:");
break;
case 1:
strcpy(s, "[info]:");
break;
case 2:
strcpy(s, "[warn]:");
break;
case 3:
strcpy(s, "[erro]:");
break;
default:
strcpy(s, "[info]:");
break;
}
//写入一个log,对m_count++
m_mutex.lock();//m_count和m_fp是共享的,要用锁修改,这就表明上面的时间是调用的时间而不是写的时间,因为锁要阻塞耗时
m_count++;//先++,因为是从0开始的,++后判断是否到最大行数了


//日志不是今天或写入的日志行数是最大行的倍数,这个时候要新换一个日志文件
//m_split_lines为最大行数
if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0)
{

char new_log[256] = {0};//日志内容
fflush(m_fp);//把缓冲区的内容强制写入文件,准备换新文件了
fclose(m_fp);//关闭
char tail[16] = {0};//时间信息

snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);//02表示月份和日期以两位数的形式呈现

if (m_today != my_tm.tm_mday)//新的一天,换一个文件
{
snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);//这里可能有点问题,因为dirname和logname不一定有,如果前面p是NULL的话,那么新的文件就只有日期了,前面最好更新一个logname
m_today = my_tm.tm_mday;
m_count = 0;
}
else//这一天的日志行数太多了,要分文件,m_count / m_split_lines表示这是第几份
{
snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines);
}
m_fp = fopen(new_log, "a");//打开新文件,把日志系统当前写入的文件更换
}

m_mutex.unlock();

va_list valst;//解决变参问题的宏,下面介绍
va_start(valst, format);//初始化,指向第一个参数地址

string log_str;
//接下来开始写内容
m_mutex.lock();//写缓冲区,要锁

//写入的具体时间内容格式
int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",
my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);//前面分级的s在这里出现,它是内容开头

//时间、级别都写进缓冲区之后,把内容写入,内容就是可变参数,通过valst写入
int m = vsnprintf(m_buf + n, m_log_buf_size - 1, format, valst);//该函数后面介绍
m_buf[n + m] = '\n';//添加一个换行
m_buf[n + m + 1] = '\0';//这一个缓冲区字符串结束
log_str = m_buf;//变成string

m_mutex.unlock();

if (m_is_async && !m_log_queue->full())//如果是异步的且阻塞队列有空间
{
m_log_queue->push(log_str);//把写的任务推入队列,参数就是要写的全部内容,不执行写的功能
}
else//同步的话或者阻塞队列已经满了就直接写
{
m_mutex.lock();//互斥写入文件中
fputs(log_str.c_str(), m_fp);
m_mutex.unlock();
}

va_end(valst);//清空参数列表
}

void Log::flush(void)
{
m_mutex.lock();
//强制刷新写入流缓冲区
fflush(m_fp);
m_mutex.unlock();
}

涉及到的与时间类相关的知识:

  • struct tm *localtime(const time_t *timer)

  • timer – 这是指向表示日历时间的 time_t 值的指针。

  • C 库函数 struct tm *localtime(const time_t *timer) 使用 timer 的值来填充 tm 结构。timer 的值被分解为 tm 结构,并用本地时区表示。

  • 该函数返回指向 tm 结构的指针,该结构带有被填充的时间信息。下面是 tm 结构的细节:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct tm {
    int tm_sec; /* 秒,范围从 0 到 59 */
    int tm_min; /* 分,范围从 0 到 59 */
    int tm_hour; /* 小时,范围从 0 到 23 */
    int tm_mday; /* 一月中的第几天,范围从 1 到 31 */
    int tm_mon; /* 月份,范围从 0 到 11 */
    int tm_year; /* 自 1900 起的年数 */
    int tm_wday; /* 一周中的第几天,范围从 0 到 6 */
    int tm_yday; /* 一年中的第几天,范围从 0 到 365 */
    int tm_isdst; /* 夏令时 */
    };
  • char *strrchr(const char *str, int c)

  • C 库函数 char *strrchr(const char *str, int c) 在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。

  • 该函数返回 str 中最后一次出现字符 c 的位置。如果未找到该值,则函数返回一个空指针。

  • int snprintf ( char * str, size_t size, const char * format, … );

  • C 库函数 int snprintf(char *str, size_t size, const char *format, …) 设将可变参数**(…)按照 format 格式化成字符串,并将字符串复制到 str 中,size** 为要写入的字符的最大数目,超过 size 会被截断。

  • 返回值

    • 1、如果格式化后的字符串长度小于 size,则会把字符串全部复制到 str 中,并给其后添加一个字符串结束符 \0
    • 2、如果格式化后的字符串长度大于等于 size,超过 size 的部分会被截断,只将其中的 (size-1) 个字符复制到 str 中,并给其后添加一个字符串结束符 \0,返回值为欲写入的字符串长度。
  • VA_LIST 解决变参问题的一组宏,所在头文件:#include <stdarg.h>,用于获取不确定个数的参数,这种获取是根据参数类型对应的大小,找到对应的内存地址,然后获取参数来实现的

  • typedef char *va_list;
    
    获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数最左边的参数)
    #define va_start(list,param1) 
    
    获取可变参数的当前参数,返回指定类型并将指针指向下一参数(mode参数描述了当前参数的类型)
    #define va_arg(list,mode)
    
    清空va_list可变参数列表
    #define va_end(list)
    
    1
    2
    3
    4
    5
    6
    7

    * ```
    va_list的使用方法:
    a) 首先在函数中定义一个具有va_list型的变量,这个变量是指向参数的指针。
    b) 然后用va_start宏初始化变量刚定义的va_list变量,使其指向第一个可变参数的地址。
    c) 然后va_arg返回可变参数,va_arg的第二个参数是你要返回的参数的类型(如果多个可变参数,依次调用va_arg获取各个参数)。
    d) 最后使用va_end宏结束可变参数的获取。
  • int vsnprintf (char * s, size_t n, const char * format, va_list arg );

  • 将格式化的数据从变量参数列表写入大小已设置的缓冲区

  • 参数

    • s

      指向存储结果C-string的缓冲区的指针。 缓冲区的大小至少应为n字符。

    • n

      缓冲区中要使用的最大字节数。 生成的字符串的长度最大为n-1,为其他终止空字符留出空间。

    • format

      包含格式字符串的C字符串,其格式与prinf相同。

    • arg

      一个值,该值标识用初始化的变量参数列表。

  • 返回值

    • 成功打印到sbuf中的字符的个数,不包括末尾追加的\0。如果格式化解析失败,则返回负数。

第四站

http——前置知识

这部分内容很多,耐心些,别急

作者写的三篇介绍:

最新版Web服务器项目详解 - 04 http连接处理(上) (qq.com)

最新版Web服务器项目详解 - 05 http连接处理(中) (qq.com)

最新版Web服务器项目详解 - 06 http连接处理(下) (qq.com)

epoll

epoll是linux新内核中替换select来做事件触发的机制,效率非常高,底层使用红黑树实现。这篇博客讲的非常清楚,强烈推荐:epoll使用详解(精髓)_ljx0305的博客-CSDN博客_epoll。下面简单介绍下API,头文件#include <sys/epoll.h>

  • int epoll_create(int size)

    • 创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数,size不起作用。(从Linux 2.6.8开始,max_size参数将被忽略,但必须大于零。)
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

    • 该函数用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除

    • epfd:为epoll_creat的句柄

    • op:表示动作,用3个宏来表示:

      • EPOLL_CTL_ADD (注册新的fd到epfd),相当于把fd加到epfd这棵红黑树上
      • EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
      • EPOLL_CTL_DEL (从epfd删除一个fd);
    • fd:文件描述符

    • event:告诉内核需要监听的事件,结构如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      typedef union epoll_data {
      void *ptr;
      int fd;
      __uint32_t u32;
      __uint64_t u64;
      } epoll_data_t;

      struct epoll_event {
      __uint32_t events; /* Epoll events,是一串比特,设置类型时把类型或起来 */
      epoll_data_t data; /* User data variable */
      };
    • events描述事件类型,其中epoll事件类型有以下几种

      • EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
      • EPOLLOUT:表示对应的文件描述符可以写
      • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
      • EPOLLERR:表示对应的文件描述符发生错误
      • EPOLLHUP:表示对应的文件描述符被挂断;
      • EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
      • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
  • int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

    • 该函数用于等待所监控文件描述符上有事件的产生,返回就绪的文件描述符个数

      • events:用来存内核得到事件的集合,

      • maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,

      • timeout:是超时时间

        • -1:阻塞
        • 0:立即返回,非阻塞
        • >0:指定毫秒
      • 返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

触发模式:

  • LT水平触发模式

    • 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll还会再次向应用程序通知此事件,直到该事件被处理完毕。
  • ET边缘触发模式

    • 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。
    • 必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain
  • ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,故效率要比LT模式高。LT模式是epoll的默认工作模式

  • EPOLLONESHOT

    • 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
    • 我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件

项目中epoll相关代码部分包括非阻塞模式、内核事件表注册事件、删除事件、重置EPOLLONESHOT事件四种。

  • 非阻塞模式

  • //对文件描述符设置非阻塞
    int setnonblocking(int fd)
    {
        int old_option = fcntl(fd, F_GETFL);//返回fd的状态标志,是一串比特位
        int new_option = old_option | O_NONBLOCK;//设置非阻塞的比特位,把前面获得的flag和它或起来就可以了
        fcntl(fd, F_SETFL, new_option);//重新设置
        return old_option;
    }
    /*
    阻塞方式是文件读写操作的默认方式,但是应用程序员可通过使用O_NONBLOCK 标志来人为
    的设置读写操作为非阻塞方式 .( 该标志定义在 < linux/fcntl.h > 中,在打开文件时指定 ) .
     
    如果设置了 O_NONBLOCK 标志,read 和 write 的行为是不同的 ,如果进程没有数据就绪时调用了 read ,
    或者在缓冲区没有空间时调用了 write ,系统只是简单的返回 EAGAIN,而不会阻塞进程.
    */
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性

    ```c++
    #include<unistd.h>
    #include<fcntl.h>
    int fcntl(int fd, int cmd);
    int fcntl(int fd, int cmd, long arg);
    int fcntl(int fd, int cmd ,struct flock* lock);
    fcntl函数功能依据cmd的值的不同而不同。参数对应功能如下:
    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
    (1)F_DUPFD
    与dup函数功能一样,复制由fd指向的文件描述符,调用成功后返回新的文件描述符,与旧的文件描述符共同指向同一个文件。

    (2)F_GETFD
    读取文件描述符close-on-exec标志。
    close_on_exec 是一个进程所有文件描述符(文件句柄)的位图标志,每个比特位代表一个打开的文件描述符,用于确定在调用系统调用execve()时需要关闭的文件句柄(参见include/fcntl.h)。当一个程序使用fork()函数创建了一个子进程时,通常会在该子进程中调用execve()函数加载执行另一个新程序。此时子进程将完全被新程序替换掉,并在子进程中开始执行新程序。若一个文件描述符在close_on_exec中的对应比特位被设置,那么在执行execve()时该描述符将被关闭,否则该描述符将始终处于打开状态。
    试想一下这样的场景:在Webserver中,首先会使用root权限启动,以此打开root权限才能打开的端口、日志等文件。然后降权到普通用户,fork出一些worker进程,这些进程中再进行解析脚本、写日志、输出结果等进一步操作。
    然而这里,就会发现隐含一个安全问题:子进程中既然继承了父进程的FD,那么子进程中运行的脚本只需要继续操作这些FD,就能够使用普通权限“越权”操作root用户才能操作的文件。

    (3)F_SETFD
    将文件描述符close-on-exec标志设置为第三个参数arg的最后一位

    (4)F_GETFL
    获取文件打开方式的标志,标志值含义与open调用一致

    (5)F_SETFL
    设置文件打开方式标志为arg指定方式

    (6)F_SETLK
    此时fcntl函数用来设置或释放锁。当short_l_type为F_RDLCK为读锁,F_WDLCK为写锁,F_UNLCK为解锁。
    如果锁被其他进程占用,则返回-1;
    这种情况设的锁遇到锁被其他进程占用时,会立刻停止进程。

    (7)F_SETLKW
    此时也是给文件上锁,不同于F_SETLK的是,该上锁是阻塞方式。当希望设置的锁因为其他锁而被阻止设置时,该命令会等待相冲突的锁被释放。

    (8)F_GETLK
    第3个参数lock指向一个希望设置的锁的属性结构,如果锁能被设置,该命令并不真的设置锁,而是只修改lock的l_type为F_UNLCK,然后返回该结构体。如果存在一个或多个锁与希望设置的锁相互冲突,则fcntl返回其中的一个锁的flock结构。
    返回值:与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列三个命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一个返回新的文件描述符,第二个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。
  • 内核事件表注册新事件,开启EPOLLONESHOT,针对客户端连接的描述符,listenfd不用开启

  •  void addfd(int epollfd, int fd, bool one_shot)
     {
         epoll_event event;
         event.data.fd = fd;
     
     #ifdef ET
         event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;//告知要监听可读事件、文件描述符被挂断事件
     #endif
     
    #ifdef LT
        event.events = EPOLLIN | EPOLLRDHUP;//可读、文件描述符被挂断
    #endif
    
        if (one_shot)
            event.events |= EPOLLONESHOT;
        epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);//处理好描述的事件类型后,添加进内核事件表的文件描述符epfd
        setnonblocking(fd);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    * 内核事件表删除事件

    * ```c++
    void removefd(int epollfd, int fd)
    {
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);//标志是delete,删除这个fd
    close(fd);
    }
  • 重置EPOLLONESHOT事件

  • ```c++
    void modfd(int epollfd, int fd, int ev)
    {
    epoll_event event;
    event.data.fd = fd;
    #ifdef ET
    event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
    #endif

    #ifdef LT
    event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
    #endif

    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
    

    }

    1
    2
    3
    4
    5

    ### http细节

    请求报文:get和post,报文的请求头部不一定全部都有,但可以有:

    GET /562f25980001b1b106000338.jpg HTTP/1.1
    Host:img.mukewang.com
    User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
    AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
    Accept:image/webp,image/,/*;q=0.8
    Referer:http://www.imooc.com/
    Accept-Encoding:gzip, deflate, sdch
    Accept-Language:zh-CN,zh;q=0.8
    空行
    请求数据为空

1

POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

- **请求行**,用来说明请求类型,要访问的资源以及所使用的HTTP版本。
GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。

- **请求头部**,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。

- - HOST,给出请求资源所在服务器的域名。
- User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
- Accept,说明用户代理可处理的媒体类型。
- Accept-Encoding,说明用户代理支持的内容编码。
- Accept-Language,说明用户代理能够处理的自然语言集。
- Content-Type,说明实现主体的媒体类型。
- Content-Length,说明实现主体的大小。
- Connection,连接管理,可以是Keep-Alive或close。

- **空行**,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。

- **请求数据**也叫主体,可以添加任意的其他数据。

响应报文:

HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
空行

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

- 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。
- 消息报头,用来说明客户端要使用的一些附加信息。
第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8。
- 空行,消息报头后面的空行是必须的。
- 响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文。

状态码:

HTTP有5种类型的状态码,具体的:

- 1xx:指示信息--表示请求已接收,继续处理。

- 2xx:成功--表示请求正常处理完毕。

- - 200 OK:客户端请求被正常处理。
- 206 Partial content:客户端进行了范围请求。

- 3xx:重定向--要完成请求必须进行更进一步的操作。

- - 301 Moved Permanently:永久重定向,该资源已被永久移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一。
- 302 Found:临时重定向,请求的资源现在临时从不同的URI中获得。

- 4xx:客户端错误--请求有语法错误,服务器无法处理请求。

- - 400 Bad Request:请求报文存在语法错误。
- 403 Forbidden:请求被服务器拒绝。
- 404 Not Found:请求不存在,服务器上找不到请求的资源。

- 5xx:服务器端错误--服务器处理请求出错。

- - 500 Internal Server Error:服务器在执行请求时出现错误。

http报文处理流程:

- 浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。
- 工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。
- 解析完之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器端。

### http类

这一部分代码在TinyWebServer/http/http_conn.h中,主要是http类的定义。

```c++
#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>//存储错误
#include <sys/wait.h>
#include <sys/uio.h>
#include <map>

#include "../lock/locker.h"
#include "../CGImysql/sql_connection_pool.h"
#include "../timer/lst_timer.h"
#include "../log/log.h"

class http_conn
{
public:
//设置读取文件的名称m_real_file大小
static const int FILENAME_LEN = 200;
//设置读缓冲区m_read_buf大小
static const int READ_BUFFER_SIZE = 2048;
//设置写缓冲区m_write_buf大小
static const int WRITE_BUFFER_SIZE = 1024;

//报文的请求方法,本项目只用到GET和POST
enum METHOD
{
GET = 0,
POST,
HEAD,
PUT,
DELETE,
TRACE,
OPTIONS,
CONNECT,
PATH
};
//主状态机的状态
enum CHECK_STATE
{
CHECK_STATE_REQUESTLINE = 0,
CHECK_STATE_HEADER,
CHECK_STATE_CONTENT
};
//报文解析的结果
enum HTTP_CODE
{
NO_REQUEST,
GET_REQUEST,
BAD_REQUEST,
NO_RESOURCE,
FORBIDDEN_REQUEST,
FILE_REQUEST,
INTERNAL_ERROR,
CLOSED_CONNECTION
};
//从状态机的状态
enum LINE_STATUS
{
LINE_OK = 0,
LINE_BAD,
LINE_OPEN
};

public:
http_conn() {}
~http_conn() {}

public:
//初始化套接字地址,函数内部会调用私有方法init
void init(int sockfd, const sockaddr_in &addr, char *, int, int, string user, string passwd, string sqlname);
//关闭http连接
void close_conn(bool real_close = true);

void process();

//读取浏览器端发来的全部数据
bool read_once();
//响应报文写入函数
bool write();

sockaddr_in *get_address()
{
return &m_address;
}

//同步线程初始化数据库读取表
void initmysql_result(connection_pool *connPool);

int timer_flag;
int improv;


private:
void init();
//从m_read_buf读取,并处理请求报文
HTTP_CODE process_read();
//向m_write_buf写入响应报文数据
bool process_write(HTTP_CODE ret);
//主状态机解析报文中的请求行数据
HTTP_CODE parse_request_line(char *text);
//主状态机解析报文中的请求头数据
HTTP_CODE parse_headers(char *text);
//主状态机解析报文中的请求内容
HTTP_CODE parse_content(char *text);
//生成响应报文
HTTP_CODE do_request();

//m_start_line是已经解析的字符
//get_line用于将指针向后偏移,指向未处理的字符
char *get_line() { return m_read_buf + m_start_line; };

//从状态机读取一行,分析是请求报文的哪一部分
LINE_STATUS parse_line();
void unmap();

//根据响应报文格式,生成对应8个部分,以下函数均由do_request调用
bool add_response(const char *format, ...);
bool add_content(const char *content);
bool add_status_line(int status, const char *title);
bool add_headers(int content_length);
bool add_content_type();
bool add_content_length(int content_length);
bool add_linger();
bool add_blank_line();

public:
static int m_epollfd;//内核事件表,类共享的
static int m_user_count;//记录总数,静态变量的形式
MYSQL *mysql;
int m_state; //读为0, 写为1

private:
int m_sockfd;
sockaddr_in m_address;

//存储读取的请求报文数据
char m_read_buf[READ_BUFFER_SIZE];
//缓冲区中m_read_buf中数据的最后一个字节的下一个位置
int m_read_idx;
//m_read_buf读取的位置m_checked_idx
int m_checked_idx;
//m_read_buf中已经解析的字符个数
int m_start_line;

//存储发出的响应报文数据
char m_write_buf[WRITE_BUFFER_SIZE];
//指示buffer中的长度
int m_write_idx;

//主状态机的状态
CHECK_STATE m_check_state;
//请求方法
METHOD m_method;

//以下为解析请求报文中对应的6个变量
//存储读取文件的名称
char m_real_file[FILENAME_LEN];
char *m_url;
char *m_version;
char *m_host;
int m_content_length;
bool m_linger;

//读取服务器上的文件地址
char *m_file_address;

//stat iovec后面介绍
struct stat m_file_stat;//获取文件的信息
//io向量机制iovec
struct iovec m_iv[2];
int m_iv_count;
int cgi; //是否启用的POST
char *m_string; //存储请求头数据
int bytes_to_send;//剩余发送字节数
int bytes_have_send;//已发送字节数
char *doc_root;

map<string, string> m_users;
int m_TRIGMode;
int m_close_log;

char sql_user[100];
char sql_passwd[100];
char sql_name[100];
};

#endif

在http请求接收部分,会涉及到init和read_once函数,但init仅仅是对私有成员变量进行初始化,不用过多讲解。

这里,对read_once进行介绍。read_once读取浏览器端发送来的请求报文,直到无数据可读或对方关闭连接,读取到m_read_buffer中,并更新m_read_idx。

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
 //循环读取客户数据,直到无数据可读或对方关闭连接
bool http_conn::read_once()
{
if(m_read_idx>=READ_BUFFER_SIZE)
{
return false;
}
int bytes_read=0;
while(true)
{
//从套接字接收数据,存储在m_read_buf缓冲区
bytes_read=recv(m_sockfd,m_read_buf+m_read_idx,READ_BUFFER_SIZE-m_read_idx,0);//该函数后面介绍
if(bytes_read==-1)
{
//非阻塞ET模式下,需要一次性将数据读完
if(errno==EAGAIN||errno==EWOULDBLOCK)//这种错误是系统告知要再尝试一次,可能是因为没有数据了,break返回true
break;
return false;
}
else if(bytes_read==0)
{
return false;
}
//修改m_read_idx的读取字节数
m_read_idx+=bytes_read;
}
return true;
}
  • int recv(int sockfd, char * buf, int len, int flags);

    • sockfd:连接的fd
    • buf:用于接收数据的缓冲区
    • len:缓冲区长度,一般是参数2的字节数-1,把\0字符串结尾留出来
    • flags:指定调用方式,一般设置为0
    • 返回值:成功返回实际读到的字节数。如果recv在copy时出错,那么它返回err,err小于0;如果recv函数在等待协议接收数据时网络中断了,那么它返回0 。
  • 在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。EAGAIN和 EWOULDBLOCK等效!

    • 从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。例如,以O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。
    • 这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以。对非阻塞socket而言,EAGAIN不是一种错误。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。
  • struct iovec 结构体定义了一个向量元素,通常这个 iovec 结构体用于一个多元素的数组,对于每一个元素,iovec 结构体的字段 iov_base 指向一个缓冲区,这个缓冲区存放的是网络接收的数据(read),或者网络将要发送的数据(write)。iovec 结构体的字段 iov_len 存放的是接收数据的最大长度(read),或者实际写入的数据长度(write)。

    • struct iovec {
          /* Starting address (内存起始地址)*/
          void  *iov_base;   
      
          /* Number of bytes to transfer(这块内存长度) */
          size_t iov_len;  
      };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20

      * struct stat这个结构体是用来描述一个linux系统文件系统中的文件属性的结构。

      * ```c++
      struct stat
      {
      dev_t st_dev; /* ID of device containing file -文件所在设备的ID*/
      ino_t st_ino; /* inode number -inode节点号*/
      mode_t st_mode; /* protection -保护模式?*/
      nlink_t st_nlink; /* number of hard links -链向此文件的连接数(硬连接)*/
      uid_t st_uid; /* user ID of owner -user id*/
      gid_t st_gid; /* group ID of owner - group id*/
      dev_t st_rdev; /* device ID (if special file) -设备号,针对设备文件*/
      off_t st_size; /* total size, in bytes -文件大小,字节为单位*/
      blksize_t st_blksize; /* blocksize for filesystem I/O -系统块的大小*/
      blkcnt_t st_blocks; /* number of blocks allocated -文件所占块数*/
      time_t st_atime; /* time of last access -最近存取时间*/
      time_t st_mtime; /* time of last modification -最近修改时间*/
      time_t st_ctime; /* time of last status change - */
      };
    • //_stat函数用来获取指定路径的文件或者文件夹的信息。
      
      
      //! 需要包含de头文件  
      #include <sys/types.h>    
      #include <sys/stat.h>   
      int stat(
        const char *filename    //文件或者文件夹的路径
        , struct stat *buf      //获取的信息保存在内存中
      ); //! prototype,原型     
      
      //正确——返回0
      //错误——返回-1,具体错误码保存在errno中
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      * 一般情况下,我们关心文件大小和创建时间、访问时间、修改时间。

      * #### mmap

      * 用于将一个文件或其他对象映射到内存,提高文件的访问速度。

      * ```c++
      void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
      int munmap(void* start,size_t length);
    • start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址

    • length:映射区的长度

    • prot:期望的内存保护标志,不能与文件的打开模式冲突

      • PROT_READ 表示页内容可以被读取
    • flags:指定映射对象的类型,映射选项和映射页是否可以共享

      • MAP_PRIVATE 建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件
    • fd:有效的文件描述符,一般是由open()函数返回

    • off_toffset:被映射对象内容的起点

    • 返回值:成功返回创建的映射区的首地址;失败返回宏MAP_FAILED。

  • writev

    • writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写。

    • ```c++
      #include <sys/uio.h>
      ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);

      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

      * filedes表示文件描述符

      * iov为前述io向量机制结构体iovec

      * iovcnt为结构体的个数

      * 若成功则返回已写的字节数,若出错则返回-1。`writev`以顺序`iov[0]`,`iov[1]`至`iov[iovcnt-1]`从缓冲区中聚集输出数据。`writev`返回输出的字节总数,通常,它应等于所有缓冲区长度之和。

      * **特别注意:** 循环调用writev时,需要重新处理iovec中的指针和长度,该函数不会对这两个成员做任何处理。writev的返回值为已写的字节数,但这个返回值“实用性”并不高,因为参数传入的是iovec数组,计量单位是iovcnt,而不是字节数,我们仍然需要通过遍历iovec来计算新的基址,另外写入数据的“结束点”可能位于一个iovec的中间某个位置,因此需要调整临界iovec的io_base和io_len。


      补充:

      Linux中系统调用的错误都存储于 `errno`中,`errno`由操作系统维护,存储就近发生的错误,即下一次的错误码会覆盖掉上一次的错误。

      > *PS: 只有当系统调用或者调用lib函数时出错,才会置位`errno`!*

      查看系统中所有的`errno`所代表的含义,可以采用如下的代码:

      ```c++
      /* Function: obtain the errno string
      * char *strerror(int errno)
      */

      #include <stdio.h>
      #include <string.h> //for strerror()
      //#include <errno.h>
      int main()
      {
      int tmp = 0;
      for(tmp = 0; tmp <=256; tmp++)
      {
      printf("errno: %2d\t%s\n",tmp,strerror(tmp));
      }
      return 0;
      }
      //输出信息如下:
      errno: 0 Success
      errno: 1 Operation not permitted
      errno: 2 No such file or directory
      errno: 3 No such process
      errno: 4 Interrupted system call
      errno: 5 Input/output error
      errno: 6 No such device or address
      errno: 7 Argument list too long
      errno: 8 Exec format error
      errno: 9 Bad file descriptor
      errno: 10 No child processes
      errno: 11 Resource temporarily unavailable
      errno: 12 Cannot allocate memory
      errno: 13 Permission denied
      errno: 14 Bad address
      errno: 15 Block device required
      errno: 16 Device or resource busy
      errno: 17 File exists
      errno: 18 Invalid cross-device link
      errno: 19 No such device
      errno: 20 Not a directory
      errno: 21 Is a directory
      errno: 22 Invalid argument
      errno: 23 Too many open files in system
      errno: 24 Too many open files
      errno: 25 Inappropriate ioctl for device
      errno: 26 Text file busy
      errno: 27 File too large
      errno: 28 No space left on device
      errno: 29 Illegal seek
      errno: 30 Read-only file system
      errno: 31 Too many links
      errno: 32 Broken pipe
      errno: 33 Numerical argument out of domain
      errno: 34 Numerical result out of range
      errno: 35 Resource deadlock avoided
      errno: 36 File name too long
      errno: 37 No locks available
      errno: 38 Function not implemented
      errno: 39 Directory not empty
      errno: 40 Too many levels of symbolic links
      errno: 41 Unknown error 41
      errno: 42 No message of desired type
      errno: 43 Identifier removed
      errno: 44 Channel number out of range
      errno: 45 Level 2 not synchronized
      errno: 46 Level 3 halted
      errno: 47 Level 3 reset
      errno: 48 Link number out of range
      errno: 49 Protocol driver not attached
      errno: 50 No CSI structure available
      errno: 51 Level 2 halted
      errno: 52 Invalid exchange
      errno: 53 Invalid request descriptor
      errno: 54 Exchange full
      errno: 55 No anode
      errno: 56 Invalid request code
      errno: 57 Invalid slot
      errno: 58 Unknown error 58
      errno: 59 Bad font file format
      errno: 60 Device not a stream
      errno: 61 No data available
      errno: 62 Timer expired
      errno: 63 Out of streams resources
      errno: 64 Machine is not on the network
      errno: 65 Package not installed
      errno: 66 Object is remote
      errno: 67 Link has been severed
      errno: 68 Advertise error
      errno: 69 Srmount error
      errno: 70 Communication error on send
      errno: 71 Protocol error
      errno: 72 Multihop attempted
      errno: 73 RFS specific error
      errno: 74 Bad message
      errno: 75 Value too large for defined data type
      errno: 76 Name not unique on network
      errno: 77 File descriptor in bad state
      errno: 78 Remote address changed
      errno: 79 Can not access a needed shared library
      errno: 80 Accessing a corrupted shared library
      errno: 81 .lib section in a.out corrupted
      errno: 82 Attempting to link in too many shared libraries
      errno: 83 Cannot exec a shared library directly
      errno: 84 Invalid or incomplete multibyte or wide character
      errno: 85 Interrupted system call should be restarted
      errno: 86 Streams pipe error
      errno: 87 Too many users
      errno: 88 Socket operation on non-socket
      errno: 89 Destination address required
      errno: 90 Message too long
      errno: 91 Protocol wrong type for socket
      errno: 92 Protocol not available
      errno: 93 Protocol not supported
      errno: 94 Socket type not supported
      errno: 95 Operation not supported
      errno: 96 Protocol family not supported
      errno: 97 Address family not supported by protocol
      errno: 98 Address already in use
      errno: 99 Cannot assign requested address
      errno: 100 Network is down
      errno: 101 Network is unreachable
      errno: 102 Network dropped connection on reset
      errno: 103 Software caused connection abort
      errno: 104 Connection reset by peer
      errno: 105 No buffer space available
      errno: 106 Transport endpoint is already connected
      errno: 107 Transport endpoint is not connected
      errno: 108 Cannot send after transport endpoint shutdown
      errno: 109 Too many references: cannot splice
      errno: 110 Connection timed out
      errno: 111 Connection refused
      errno: 112 Host is down
      errno: 113 No route to host
      errno: 114 Operation already in progress
      errno: 115 Operation now in progress
      errno: 116 Stale file handle
      errno: 117 Structure needs cleaning
      errno: 118 Not a XENIX named type file
      errno: 119 No XENIX semaphores available
      errno: 120 Is a named type file
      errno: 121 Remote I/O error
      errno: 122 Disk quota exceeded
      errno: 123 No medium found
      errno: 124 Wrong medium type
      errno: 125 Operation canceled
      errno: 126 Required key not available
      errno: 127 Key has expired
      errno: 128 Key has been revoked
      errno: 129 Key was rejected by service
      errno: 130 Owner died
      errno: 131 State not recoverable
      errno: 132 Operation not possible due to RF-kill
      errno: 133 Memory page has hardware error
      errno: 134~255 unknown error!

Linux中,在头文件 /usr/include/asm-generic/errno-base.h 对基础常用errno进行了宏定义

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
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H

#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */

#endif

/usr/include/asm-asm-generic/errno.h 中,对剩余的errno做了宏定义

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
#ifndef _ASM_GENERIC_ERRNO_H
#define _ASM_GENERIC_ERRNO_H

#include <asm-generic/errno-base.h>

#define EDEADLK 35 /* Resource deadlock would occur */
#define ENAMETOOLONG 36 /* File name too long */
#define ENOLCK 37 /* No record locks available */
#define ENOSYS 38 /* Function not implemented */
#define ENOTEMPTY 39 /* Directory not empty */
#define ELOOP 40 /* Too many symbolic links encountered */
#define EWOULDBLOCK EAGAIN /* Operation would block */
#define ENOMSG 42 /* No message of desired type */
#define EIDRM 43 /* Identifier removed */
#define ECHRNG 44 /* Channel number out of range */
#define EL2NSYNC 45 /* Level 2 not synchronized */
#define EL3HLT 46 /* Level 3 halted */
#define EL3RST 47 /* Level 3 reset */
#define ELNRNG 48 /* Link number out of range */
#define EUNATCH 49 /* Protocol driver not attached */
#define ENOCSI 50 /* No CSI structure available */
#define EL2HLT 51 /* Level 2 halted */
#define EBADE 52 /* Invalid exchange */
#define EBADR 53 /* Invalid request descriptor */
#define EXFULL 54 /* Exchange full */
#define ENOANO 55 /* No anode */
#define EBADRQC 56 /* Invalid request code */
#define EBADSLT 57 /* Invalid slot */

#define EDEADLOCK EDEADLK

#define EBFONT 59 /* Bad font file format */
#define ENOSTR 60 /* Device not a stream */
#define ENODATA 61 /* No data available */
#define ETIME 62 /* Timer expired */
#define ENOSR 63 /* Out of streams resources */
#define ENONET 64 /* Machine is not on the network */
#define ENOPKG 65 /* Package not installed */
#define EREMOTE 66 /* Object is remote */
#define ENOLINK 67 /* Link has been severed */
#define EADV 68 /* Advertise error */
#define ESRMNT 69 /* Srmount error */
#define ECOMM 70 /* Communication error on send */
#define EPROTO 71 /* Protocol error */
#define EMULTIHOP 72 /* Multihop attempted */
#define EDOTDOT 73 /* RFS specific error */
#define EBADMSG 74 /* Not a data message */
#define EOVERFLOW 75 /* Value too large for defined data type */
#define ENOTUNIQ 76 /* Name not unique on network */
#define EBADFD 77 /* File descriptor in bad state */
#define EREMCHG 78 /* Remote address changed */
#define ELIBACC 79 /* Can not access a needed shared library */
#define ELIBBAD 80 /* Accessing a corrupted shared library */
#define ELIBSCN 81 /* .lib section in a.out corrupted */
#define ELIBMAX 82 /* Attempting to link in too many shared libraries */

#define ELIBEXEC 83 /* Cannot exec a shared library directly */
#define EILSEQ 84 /* Illegal byte sequence */
#define ERESTART 85 /* Interrupted system call should be restarted */
#define ESTRPIPE 86 /* Streams pipe error */
#define EUSERS 87 /* Too many users */
#define ENOTSOCK 88 /* Socket operation on non-socket */
#define EDESTADDRREQ 89 /* Destination address required */
#define EMSGSIZE 90 /* Message too long */
#define EPROTOTYPE 91 /* Protocol wrong type for socket */
#define ENOPROTOOPT 92 /* Protocol not available */
#define EPROTONOSUPPORT 93 /* Protocol not supported */
#define ESOCKTNOSUPPORT 94 /* Socket type not supported */
#define EOPNOTSUPP 95 /* Operation not supported on transport endpoint */
#define EPFNOSUPPORT 96 /* Protocol family not supported */
#define EAFNOSUPPORT 97 /* Address family not supported by protocol */
#define EADDRINUSE 98 /* Address already in use */
#define EADDRNOTAVAIL 99 /* Cannot assign requested address */
#define ENETDOWN 100 /* Network is down */
#define ENETUNREACH 101 /* Network is unreachable */
#define ENETRESET 102 /* Network dropped connection because of reset */
#define ECONNABORTED 103 /* Software caused connection abort */
#define ECONNRESET 104 /* Connection reset by peer */
#define ENOBUFS 105 /* No buffer space available */
#define EISCONN 106 /* Transport endpoint is already connected */
#define ENOTCONN 107 /* Transport endpoint is not connected */
#define ESHUTDOWN 108 /* Cannot send after transport endpoint shutdown */
#define ETOOMANYREFS 109 /* Too many references: cannot splice */
#define ETIMEDOUT 110 /* Connection timed out */
#define ECONNREFUSED 111 /* Connection refused */
#define EHOSTDOWN 112 /* Host is down */
#define EHOSTUNREACH 113 /* No route to host */
#define EALREADY 114 /* Operation already in progress */
#define EINPROGRESS 115 /* Operation now in progress */
#define ESTALE 116 /* Stale file handle */
#define EUCLEAN 117 /* Structure needs cleaning */
#define ENOTNAM 118 /* Not a XENIX named type file */
#define ENAVAIL 119 /* No XENIX semaphores available */
#define EISNAM 120 /* Is a named type file */
#define EREMOTEIO 121 /* Remote I/O error */
#define EDQUOT 122 /* Quota exceeded */

#define ENOMEDIUM 123 /* No medium found */
#define EMEDIUMTYPE 124 /* Wrong medium type */
#define ECANCELED 125 /* Operation Canceled */
#define ENOKEY 126 /* Required key not available */
#define EKEYEXPIRED 127 /* Key has expired */
#define EKEYREVOKED 128 /* Key has been revoked */
#define EKEYREJECTED 129 /* Key was rejected by service */

/* for robust mutexes */
#define EOWNERDEAD 130 /* Owner died */
#define ENOTRECOVERABLE 131 /* State not recoverable */

#define ERFKILL 132 /* Operation not possible due to RF-kill */

#define EHWPOISON 133 /* Memory page has hardware error */

#endif

http的调用

这不是http的实现,实现后面再说,这里是使用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
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
 //创建MAX_FD个http类对象
http_conn* users=new http_conn[MAX_FD];

//创建内核事件表
epoll_event events[MAX_EVENT_NUMBER];
epollfd = epoll_create(5);
assert(epollfd != -1);

//将listenfd放在epoll树上
addfd(epollfd, listenfd, false);

//将上述epollfd赋值给http类对象的m_epollfd属性
http_conn::m_epollfd = epollfd;

while (!stop_server)
{
//等待所监控文件描述符上有事件的产生
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
break;
}
//对所有就绪事件进行处理
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;//通过epollfd监听到的就绪事件会放在events数组

//处理新到的客户连接
if (sockfd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
//LT水平触发
#ifdef LT
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
continue;
}
if (http_conn::m_user_count >= MAX_FD)
{
show_error(connfd, "Internal server busy");
continue;
}
#endif

//ET非阻塞边缘触发
#ifdef ET
//需要循环接收数据
while (1)
{
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
break;
}
if (http_conn::m_user_count >= MAX_FD)
{
show_error(connfd, "Internal server busy");
break;
}
users[connfd].init(connfd, client_address);
}
continue;
#endif
}

//处理异常事件
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服务器端关闭连接
}

//处理信号
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
}

//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
//读入对应缓冲区
if (users[sockfd].read_once())
{
//若监测到读事件,将该事件放入请求队列
pool->append(users + sockfd);
}
else
{
//服务器关闭连接
}
}

}
}

http实现

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
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
#include "http_conn.h"

#include <mysql/mysql.h>
#include <fstream>

//定义http响应的一些状态信息
const char *ok_200_title = "OK";
const char *error_400_title = "Bad Request";
const char *error_400_form = "Your request has bad syntax or is inherently impossible to staisfy.\n";
const char *error_403_title = "Forbidden";
const char *error_403_form = "You do not have permission to get file form this server.\n";
const char *error_404_title = "Not Found";
const char *error_404_form = "The requested file was not found on this server.\n";
const char *error_500_title = "Internal Error";
const char *error_500_form = "There was an unusual problem serving the request file.\n";

locker m_lock;
map<string, string> users;

void http_conn::initmysql_result(connection_pool *connPool)
{
//先从连接池中取一个连接
MYSQL *mysql = NULL;
connectionRAII mysqlcon(&mysql, connPool);

//在user表中检索username,passwd数据,浏览器端输入
if (mysql_query(mysql, "SELECT username,passwd FROM user"))//从mysql这个接口输入查询语句
{
LOG_ERROR("SELECT error:%s\n", mysql_error(mysql));
}

//从表中检索完整的结果集
MYSQL_RES *result = mysql_store_result(mysql);

//返回结果集中的列数
int num_fields = mysql_num_fields(result);

//返回所有字段结构的数组
MYSQL_FIELD *fields = mysql_fetch_fields(result);

//从结果集中获取下一行,将对应的用户名和密码,存入map中
while (MYSQL_ROW row = mysql_fetch_row(result))
{
string temp1(row[0]);
string temp2(row[1]);
users[temp1] = temp2;
}
}

//对文件描述符设置非阻塞
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}

//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
//这可以把一个fd绑定在epollfd上,接下来对内核事件的操作(唤醒什么的)都是针对fd文件描述符的。
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode)
{
epoll_event event;
event.data.fd = fd;

if (1 == TRIGMode)//边缘触发模式
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
else
event.events = EPOLLIN | EPOLLRDHUP;

if (one_shot)
event.events |= EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);//add一个
setnonblocking(fd);
}

//从内核时间表删除描述符
void removefd(int epollfd, int fd)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
close(fd);//从epollfd中删掉fd,然后关闭
}

//将事件重置为EPOLLONESHOT
void modfd(int epollfd, int fd, int ev, int TRIGMode)
{
epoll_event event;
event.data.fd = fd;

if (1 == TRIGMode)
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
else
event.events = ev | EPOLLONESHOT | EPOLLRDHUP;

epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);//修改
}
//静态变量定义
int http_conn::m_user_count = 0;
int http_conn::m_epollfd = -1;

//关闭连接,关闭一个连接,客户总量减一
void http_conn::close_conn(bool real_close)
{
if (real_close && (m_sockfd != -1))
{
printf("close %d\n", m_sockfd);
removefd(m_epollfd, m_sockfd);
m_sockfd = -1;
m_user_count--;
}
}

//初始化连接,外部调用初始化套接字地址
void http_conn::init(int sockfd, const sockaddr_in &addr, char *root, int TRIGMode,
int close_log, string user, string passwd, string sqlname)
{
m_sockfd = sockfd;
m_address = addr;

addfd(m_epollfd, sockfd, true, m_TRIGMode);//注册一个连接
m_user_count++;

//当浏览器出现连接重置时,可能是网站根目录出错或http响应格式出错或者访问的文件中内容完全为空
doc_root = root;
m_TRIGMode = TRIGMode;
m_close_log = close_log;

strcpy(sql_user, user.c_str());
strcpy(sql_passwd, passwd.c_str());
strcpy(sql_name, sqlname.c_str());

init();
}

//初始化新接受的连接
//check_state默认为分析请求行状态
void http_conn::init()
{
mysql = NULL;
bytes_to_send = 0;
bytes_have_send = 0;
m_check_state = CHECK_STATE_REQUESTLINE;
m_linger = false;
m_method = GET;
m_url = 0;
m_version = 0;
m_content_length = 0;
m_host = 0;
m_start_line = 0;
m_checked_idx = 0;
m_read_idx = 0;
m_write_idx = 0;
cgi = 0;
m_state = 0;
timer_flag = 0;
improv = 0;

memset(m_read_buf, '\0', READ_BUFFER_SIZE);
memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);
memset(m_real_file, '\0', FILENAME_LEN);
}

//从状态机,用于分析出一行内容,见作者的分析中篇
//返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
http_conn::LINE_STATUS http_conn::parse_line()
{
char temp;
for (; m_checked_idx < m_read_idx; ++m_checked_idx)
{
temp = m_read_buf[m_checked_idx];//temp为将要分析的字节
if (temp == '\r')//如果当前是\r字符,则有可能会读取到完整行
{
if ((m_checked_idx + 1) == m_read_idx)//下一个字符达到了buffer结尾,则接收不完整,需要继续接收
return LINE_OPEN;
else if (m_read_buf[m_checked_idx + 1] == '\n')//下一个字符是\n,将\r\n改为\0\0
{
m_read_buf[m_checked_idx++] = '\0';
m_read_buf[m_checked_idx++] = '\0';
return LINE_OK;
}
return LINE_BAD;//如果都不符合,则返回语法错误
}
else if (temp == '\n')//如果当前字符是\n,也有可能读取到完整行
{//一般是上次读取到\r就到buffer末尾了,没有接收完整,再次接收时会出现这种情况
if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r')//前一个字符是\r,则接收完整
{
m_read_buf[m_checked_idx - 1] = '\0';
m_read_buf[m_checked_idx++] = '\0';
return LINE_OK;
}
return LINE_BAD;
}
}
return LINE_OPEN;//并没有找到\r\n,需要继续接收
}

//循环读取客户数据,直到无数据可读或对方关闭连接
//非阻塞ET工作模式下,需要一次性将数据读完
bool http_conn::read_once()
{
if (m_read_idx >= READ_BUFFER_SIZE)
{
return false;
}
int bytes_read = 0;

//LT读取数据
if (0 == m_TRIGMode)
{
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
m_read_idx += bytes_read;

if (bytes_read <= 0)
{
return false;
}

return true;
}
//ET读数据
else
{
while (true)//循环读取
{
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
if (bytes_read == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)//这时说明读完了
break;
return false;
}
else if (bytes_read == 0)
{
return false;
}
m_read_idx += bytes_read;
}
return true;
}
}

//解析http请求行,获得请求方法,目标url及http版本号
//在HTTP报文中,请求行用来说明请求类型,要访问的资源以及所使用的HTTP版本,其中各个部分之间通过\t或空格分隔。
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{ //请求行中最先含有空格和\t任一字符的位置并返回
m_url = strpbrk(text, " \t");//检索字符串 str1 中第一个匹配字符串 str2 中字符的字符
if (!m_url)//如果没有空格或\t,则报文格式有误
{
return BAD_REQUEST;
}
*m_url++ = '\0';//将该位置改为\0,用于将前面数据取出
char *method = text;
if (strcasecmp(method, "GET") == 0)//取出数据,并通过与GET和POST比较,以确定请求方式
m_method = GET;
else if (strcasecmp(method, "POST") == 0)
{
m_method = POST;
cgi = 1;
}
else
return BAD_REQUEST;

//检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标,该函数返回 str1 中第一个不在字符串 str2 中出现的字符下标。
m_url += strspn(m_url, " \t");//因为报文后面可能还有空格,跳过这些空格
m_version = strpbrk(m_url, " \t");

//使用与判断请求方式的相同逻辑,判断HTTP版本号
if (!m_version)
return BAD_REQUEST;
*m_version++ = '\0';
m_version += strspn(m_version, " \t");
if (strcasecmp(m_version, "HTTP/1.1") != 0)//只支持1.1版本
return BAD_REQUEST;

//这里主要是有些报文的请求资源中会带有http://,这里需要对这种情况进行单独处理
if (strncasecmp(m_url, "http://", 7) == 0)//对请求资源前7个字符进行判断
{
m_url += 7;
m_url = strchr(m_url, '/');
}
//同样增加https情况
if (strncasecmp(m_url, "https://", 8) == 0)
{
m_url += 8;
m_url = strchr(m_url, '/');
}

if (!m_url || m_url[0] != '/')//一般的不会带有上述两种符号,直接是单独的/或/后面带访问资源
return BAD_REQUEST;
//当url为/时,显示判断界面
if (strlen(m_url) == 1)
strcat(m_url, "judge.html");
m_check_state = CHECK_STATE_HEADER;//请求行处理完毕,将主状态机转移处理请求头
return NO_REQUEST;
}

//解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
if (text[0] == '\0')//判断是空行还是请求头
{
if (m_content_length != 0)//判断是GET还是POST请求
{
m_check_state = CHECK_STATE_CONTENT;//POST需要跳转到消息体处理状态
return NO_REQUEST;
}
return GET_REQUEST;
}
else if (strncasecmp(text, "Connection:", 11) == 0)//解析请求头部连接字段
{
text += 11;
text += strspn(text, " \t");//跳过空格和\t字符
if (strcasecmp(text, "keep-alive") == 0)//如果是长连接,则将linger标志设置为true
{
m_linger = true;
}
}
else if (strncasecmp(text, "Content-length:", 15) == 0)//解析请求头部内容长度字段
{
text += 15;
text += strspn(text, " \t");
m_content_length = atol(text);
}
else if (strncasecmp(text, "Host:", 5) == 0)//解析请求头部HOST字段
{
text += 5;
text += strspn(text, " \t");
m_host = text;
}
else
{
LOG_INFO("oop!unknow header: %s", text);
}
return NO_REQUEST;
}


http_conn::HTTP_CODE http_conn::parse_content(char *text)
{//判断http请求是否被完整读入
if (m_read_idx >= (m_content_length + m_checked_idx))
{
text[m_content_length] = '\0';
//POST请求中最后为输入的用户名和密码
m_string = text;
return GET_REQUEST;
}
return NO_REQUEST;
}
//主状态机
http_conn::HTTP_CODE http_conn::process_read()
{
LINE_STATUS line_status = LINE_OK;//初始化从状态机状态、HTTP请求解析结果
HTTP_CODE ret = NO_REQUEST;
char *text = 0;

//在GET请求报文中,每一行都是\r\n作为结束,所以对报文进行拆解时,仅用从状态机的状态line_status=parse_line())==LINE_OK语句即可。
//在POST请求报文中,消息体的末尾没有任何字符,所以不能使用从状态机的状态,这里转而使用主状态机的状态作为循环入口条件。
//解析完消息体后,报文的完整解析就完成了,但此时主状态机的状态还是CHECK_STATE_CONTENT,符合循环入口条件,还会再次进入循环,这并不是我们所希望的
//为此,增加了该语句,并在完成消息体解析后,将line_status变量更改为LINE_OPEN,此时可以跳出循环,完成报文解析任务。

//只有当从状态机处理好了,主状态机才运行
while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))
{
text = get_line();
//m_start_line是每一个数据行在m_read_buf中的起始位置
//m_checked_idx表示从状态机在m_read_buf中读取的位置
m_start_line = m_checked_idx;
LOG_INFO("%s", text);
switch (m_check_state)//主状态机的三种状态转移逻辑
{
case CHECK_STATE_REQUESTLINE:
{ //解析请求行
ret = parse_request_line(text);
if (ret == BAD_REQUEST)
return BAD_REQUEST;
break;
}
case CHECK_STATE_HEADER:
{ //解析请求头
ret = parse_headers(text);
if (ret == BAD_REQUEST)
return BAD_REQUEST;
//完整解析GET请求后,跳转到报文响应函数
else if (ret == GET_REQUEST)
{
return do_request();
}
break;
}
case CHECK_STATE_CONTENT:
{ //解析消息体
ret = parse_content(text);
//完整解析POST请求后,跳转到报文响应函数
if (ret == GET_REQUEST)
return do_request();//正确的请求就转调用
line_status = LINE_OPEN;//从状态机没处理好,退出循环,openline
break;
}
default:
return INTERNAL_ERROR;
}
}
return NO_REQUEST;
}
//这里跟html有关系
http_conn::HTTP_CODE http_conn::do_request()
{
strcpy(m_real_file, doc_root);//将初始化的m_real_file赋值为网站根目录
int len = strlen(doc_root);
//printf("m_url:%s\n", m_url);
const char *p = strrchr(m_url, '/');//找到m_url中/的位置

//处理cgi
if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3'))//实现登录和注册校验
{

//根据标志判断是登录检测还是注册检测
char flag = m_url[1];

char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/");
strcat(m_url_real, m_url + 2);
strncpy(m_real_file + len, m_url_real, FILENAME_LEN - len - 1);
free(m_url_real);

//将用户名和密码提取出来
//user=123&passwd=123
char name[100], password[100];
int i;
for (i = 5; m_string[i] != '&'; ++i)
name[i - 5] = m_string[i];
name[i - 5] = '\0';

int j = 0;
for (i = i + 10; m_string[i] != '\0'; ++i, ++j)
password[j] = m_string[i];
password[j] = '\0';

if (*(p + 1) == '3')
{
//如果是注册,先检测数据库中是否有重名的
//没有重名的,进行增加数据
char *sql_insert = (char *)malloc(sizeof(char) * 200);
strcpy(sql_insert, "INSERT INTO user(username, passwd) VALUES(");
strcat(sql_insert, "'");
strcat(sql_insert, name);
strcat(sql_insert, "', '");
strcat(sql_insert, password);
strcat(sql_insert, "')");

if (users.find(name) == users.end())//没有这个名字
{
m_lock.lock();//操作数据库,互斥
int res = mysql_query(mysql, sql_insert);//数据库,没有给mysql变量赋一个连接啊?
users.insert(pair<string, string>(name, password));//map
m_lock.unlock();

if (!res)
strcpy(m_url, "/log.html");
else
strcpy(m_url, "/registerError.html");
}
else
strcpy(m_url, "/registerError.html");
}
//如果是登录,直接判断
//若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0
else if (*(p + 1) == '2')
{
if (users.find(name) != users.end() && users[name] == password)
strcpy(m_url, "/welcome.html");
else
strcpy(m_url, "/logError.html");
}
}

if (*(p + 1) == '0')//如果请求资源为/0,表示跳转注册界面
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/register.html");
//将网站目录和/register.html进行拼接,更新到m_real_file中
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}
else if (*(p + 1) == '1')//如果请求资源为/1,表示跳转登录界面
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/log.html");
//将网站目录和/log.html进行拼接,更新到m_real_file中
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}
else if (*(p + 1) == '5')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/picture.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}
else if (*(p + 1) == '6')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/video.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}
else if (*(p + 1) == '7')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/fans.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}
else//如果以上均不符合,即不是登录和注册,直接将url与网站目录拼接,这里的情况是welcome界面,请求服务器上的一个图片
strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);

//通过stat获取请求资源文件信息,成功则将信息更新到m_file_stat结构体
//失败返回NO_RESOURCE状态,表示资源不存在
if (stat(m_real_file, &m_file_stat) < 0)
return NO_RESOURCE;

//判断文件的权限,是否可读,不可读则返回FORBIDDEN_REQUEST状态
if (!(m_file_stat.st_mode & S_IROTH))
return FORBIDDEN_REQUEST;

//判断文件类型,如果是目录,则返回BAD_REQUEST,表示请求报文有误
if (S_ISDIR(m_file_stat.st_mode))
return BAD_REQUEST;

//以只读方式获取文件描述符,通过mmap将该文件映射到内存中
int fd = open(m_real_file, O_RDONLY);
m_file_address = (char *)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);//避免文件描述符的浪费和占用
return FILE_REQUEST;//表示请求文件存在,且可以访问
}
void http_conn::unmap()//解除内存映射
{
if (m_file_address)
{
munmap(m_file_address, m_file_stat.st_size);
m_file_address = 0;
}
}
bool http_conn::write()
{
int temp = 0;

//若要发送的数据长度为0
//表示响应报文为空,一般不会出现这种情况
if (bytes_to_send == 0)
{
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
init();
return true;
}

while (1)
{
//将响应报文的状态行、消息头、空行和响应正文发送给浏览器端
temp = writev(m_sockfd, m_iv, m_iv_count);

if (temp < 0)//发送失败
{ //判断缓冲区是否满了
if (errno == EAGAIN)
{ //重新注册写事件
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
return true;
}
//如果发送失败,但不是缓冲区问题,取消映射
unmap();
return false;
}

//正常发送,temp为发送的字节数
bytes_have_send += temp;//更新已发送字节
bytes_to_send -= temp;//偏移文件iovec的指针

//第一个iovec头部信息的数据已发送完,发送第二个iovec数据
if (bytes_have_send >= m_iv[0].iov_len)
{
m_iv[0].iov_len = 0;//不再继续发送头部信息
m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
m_iv[1].iov_len = bytes_to_send;
}
else//继续发送第一个iovec头部信息的数据
{
m_iv[0].iov_base = m_write_buf + bytes_have_send;
m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;
}
//判断条件,数据已全部发送完
if (bytes_to_send <= 0)
{
unmap();
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);//在epoll树上重置EPOLLONESHOT事件

if (m_linger)//浏览器的请求为长连接
{
init();//重新初始化HTTP对象
return true;
}
else
{
return false;
}
}
}
}
bool http_conn::add_response(const char *format, ...)
{
if (m_write_idx >= WRITE_BUFFER_SIZE)//如果写入内容超出m_write_buf大小则报错
return false;
va_list arg_list;//定义可变参数列表
va_start(arg_list, format);//将变量arg_list初始化为传入参数
//将数据format从可变参数列表写入缓冲区,返回写入数据的长度
int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
//如果写入的数据长度超过缓冲区剩余空间,则报错
if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx))
{
va_end(arg_list);
return false;
}
m_write_idx += len;//更新m_write_idx位置
va_end(arg_list);//清空可变参列表

LOG_INFO("request:%s", m_write_buf);

return true;
}
bool http_conn::add_status_line(int status, const char *title)//添加状态行
{
return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}
bool http_conn::add_headers(int content_len)//添加消息报头,具体的添加文本长度、连接状态和空行
{
return add_content_length(content_len) && add_linger() &&
add_blank_line();
}
bool http_conn::add_content_length(int content_len)//添加Content-Length,表示响应报文的长度
{
return add_response("Content-Length:%d\r\n", content_len);
}
bool http_conn::add_content_type()//添加文本类型,这里是html
{
return add_response("Content-Type:%s\r\n", "text/html");
}
bool http_conn::add_linger()//添加连接状态,通知浏览器端是保持连接还是关闭
{
return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");
}
bool http_conn::add_blank_line()//添加空行
{
return add_response("%s", "\r\n");
}
bool http_conn::add_content(const char *content)//添加文本content
{
return add_response("%s", content);
}
bool http_conn::process_write(HTTP_CODE ret)//逻辑上处理要写什么
{
switch (ret)
{
case INTERNAL_ERROR://内部错误,500
{
add_status_line(500, error_500_title);//状态行
add_headers(strlen(error_500_form));//消息报头
if (!add_content(error_500_form))
return false;
break;
}
case BAD_REQUEST://报文语法有误,404
{
add_status_line(404, error_404_title);
add_headers(strlen(error_404_form));
if (!add_content(error_404_form))
return false;
break;
}
case FORBIDDEN_REQUEST://资源没有访问权限,403
{
add_status_line(403, error_403_title);
add_headers(strlen(error_403_form));
if (!add_content(error_403_form))
return false;
break;
}
case FILE_REQUEST://文件存在,200
{
add_status_line(200, ok_200_title);
if (m_file_stat.st_size != 0)//如果请求的资源存在
{
add_headers(m_file_stat.st_size);
//第一个iovec指针指向响应报文缓冲区,长度指向m_write_idx
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
//第二个iovec指针指向mmap返回的文件指针,长度指向文件大小
m_iv[1].iov_base = m_file_address;
m_iv[1].iov_len = m_file_stat.st_size;
m_iv_count = 2;
//发送的全部数据为响应报文头部信息和文件大小
bytes_to_send = m_write_idx + m_file_stat.st_size;
return true;
}
else
{ //如果请求的资源大小为0,则返回空白html文件
const char *ok_string = "<html><body></body></html>";
add_headers(strlen(ok_string));
if (!add_content(ok_string))
return false;
}
}
default:
return false;
}
//除FILE_REQUEST状态外,其余状态只申请一个iovec,指向响应报文缓冲区
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
m_iv_count = 1;
bytes_to_send = m_write_idx;
return true;
}
void http_conn::process()//对读事件完成最终处理并发送响应报文
{
HTTP_CODE read_ret = process_read();
if (read_ret == NO_REQUEST)//NO_REQUEST,表示请求不完整,需要继续接收请求数据
{
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);//注册并监听读事件
return;
}
bool write_ret = process_write(read_ret);//调用process_write完成报文响应
if (!write_ret)
{
close_conn();//如果写错误就关闭连接,会把fd删除且关闭
}
//?都关闭了且没有注册怎么修改
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);//注册并监听写事件
}
  • MYSQL_RES *mysql_store_result(MYSQL *mysql)
    • 对于成功检索了数据的每个查询(SELECT、SHOW、DESCRIBE、EXPLAIN、CHECK TABLE等),必须调用mysql_store_result()或mysql_use_result() 。
    • 对于其他查询,不需要调用mysql_store_result()或mysql_use_result(),但是如果在任何情况下均调用了mysql_store_result(),它也不会导致任何伤害或性能降低。通过检查mysql_store_result()是否返回0,可检测查询是否没有结果集(以后会更多)。
    • 如果希望了解查询是否应返回结果集,可使用mysql_field_count()进行检查。
  • unsigned int mysql_field_count(MYSQL *mysql)
    • 返回作用在连接上的最近查询的列数。
  • MYSQL_FIELD *mysql_fetch_field(MYSQL_RES *result)
    • 返回采用MYSQL_FIELD结构的结果集的列。重复调用该函数,以检索关于结果集中所有列的信息。
  • MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)
    • 检索结果集的下一行。在mysql_store_result()之后使用时,如果没有要检索的行,mysql_fetch_row()返回NULL。在mysql_use_result()之后使用时,如果没有要检索的行或出现了错误,mysql_fetch_row()返回NULL。行内值的数目由mysql_num_fields(result)给出。
  • int munmap(void *start,size_t length);
    • 函数说明 munmap()用来取消参数start所指的映射内存起始地址,参数length则是欲取消的内存大小。当进程结束或利用exec相关函数来执行其他程序时,映射内存会自动解除,但关闭对应的文件描述词时不会解除映射。
    • 返回值 如果解除映射成功则返回0,否则返回-1,错误原因存于errno中错误代码EINVAL参数 start或length 不合法。

第五站

项目中使用的是SIGALRM信号,具体的,利用alarm函数周期性地触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。

定时器处理非活动连接模块,主要分为两部分,其一为定时方法与信号通知流程,其二为定时器及其容器设计与定时任务的处理。

定时器与信号API

1
2
3
4
5
6
7
- Linux中的信号是一种消息处理机制,它本质上是一个整数,不同的信号对应不同的值,信号在系统中的优先级是非常高的。
- 项目中使用的信号
1. SIGALRM:定时器超时信号,超时的时间由系统调用alarm设置,默认终止进程。
2. SIGTERM:程序结束信号,kill或Ctrl+C触发,默认终止进程。
- 两个特殊信号
1. SIGKILL:9号信号,无条件终止进程,不能被捕捉、阻塞和忽略。
2. SIGSTOP:19号信号,无条件暂停进程,不能被捕捉、阻塞和忽略。
  • 还有一个信号:SIGPIPE:当服务器close一个连接时,若client端接着发数据。根据TCP 协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。
    • TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包. 按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 一个端点无法获知对端的socket是调用了close还是shutdown.
    • 对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出.
    • 为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号处理函数:signal(SIGPIPE, SIG_IGN);SIG_IGN表示忽略信号
  • Linux中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略某些信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。
  • 程序中的信号捕捉是一个注册的动作,提前告诉应用程序信号产生之后的处理动作,当进程中对应的信号产生了,这个处理动作也就被调用了。
  • sigaction结构体:

    • struct sigaction
      {
          void (*sa_handler)(int);                        // 函数指针,指向信号处理函数。
          void (*sa_sigaction)(int, siginfo_t *, void *); // 函数指针,指向信号处理函数,有三个参数。
          sigset_t sa_mask;                               // 在信号处理函数执行期间,临时屏蔽的信号。
          int sa_flags;                                   // 用于指定信号处理的行为
          void (*sa_restorer)(void);                      // 被废弃的成员,一般不使用
      };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17

      * flag有以下几个:

      * SA_RESTART,使被信号打断的系统调用自动重新发起
      * SA_NOCLDSTOP,使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
      * SA_NOCLDWAIT,使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
      * SA_NODEFER,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
      * SA_RESETHAND,信号处理之后重新设置为默认的处理方式

      * sigaction函数:

      * ```c++
      int sigaction(
      int signum, // 要捕捉的信号。
      const struct sigaction *act, // 对信号设置新的处理方式。
      struct sigaction *oldact // 上一次信号处理方式,一般指定为NULL。
      );
    • 返回值,0 表示成功,-1 表示有错误发生。

  • sigfillset函数:

    • ```c++
      int sigfillset(sigset_t * set);
      1
      2
      3
      4
      5
      6
      7
      8

      * sigfillset()用来将参数set信号集初始化,然后把所有的信号加入到此信号集里,即将所有的信号标志位置为1,屏蔽所有的信号。信号集是在执行信号处理程序时被阻塞的信号集。因此,当执行信号处理程序时,所有信号都被阻塞,不必担心另一个信号会中断信号处理程序。

      * SIGALRM、SIGTERM信号,是整形数

      * ```c++
      #define SIGALRM 14 //由alarm系统调用产生timer时钟信号
      #define SIGTERM 15 //终端发送的终止信号
  • alarm函数

    • #include<unistd.h>
      unsigned int alarm(unsigned int seconds);
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

      * alarm也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM信号。可以设置忽略或者不捕获此信号,如果采用默认方式其动作是终止调用该alarm函数的进程。

      * 要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。需要注意的是,经过指定的秒数后,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一些时间。

      * 返回值:成功:如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。

      * socketpair函数

      * 在linux下,使用socketpair函数能够创建一对套接字进行通信,项目中使用管道通信。socketpair创建的描述符任意一端既可以读也可以写。

      * ```c++
      #include <sys/types.h>
      #include <sys/socket.h>

      int socketpair(int domain, int type, int protocol, int sv[2]);
      * domain表示协议族,PF_UNIX或者AF_UNIX,AF = Address Family、PF = Protocol Family。PF_UNIX (也称作 PF_LOCAL ) 套接字族用来在同一机器上的提供有效的进程间通讯。AF\_和PF\_的值直接可以替换,没有其它区别。 * type表示协议,可以是SOCK_STREAM或者SOCK_DGRAM,SOCK_STREAM基于TCP,SOCK_DGRAM基于UDP * protocol表示类型,只能为0 * sv[2]表示套节字柄对,该两个句柄作用相同,均能进行读写双向操作
    • 返回结果, 0为创建成功,-1为创建失败

  • send函数,当套接字发送缓冲区变满时,send通常会阻塞,除非套接字设置为非阻塞模式,当缓冲区变满时,返回EAGAIN或者EWOULDBLOCK错误,此时可以调用select函数来监视何时可以发送数据。

    • ```c++
      #include <sys/types.h>
      #include <sys/socket.h>

      ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);

      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

      * sockfd:指定发送端套接字描述符。
      * buff: 存放要发送数据的缓冲区
      * nbytes: 实际要改善的数据的字节数
      * flags: 一般设置为0

      * 1) send先比较发送数据的长度nbytes和套接字sockfd的发送缓冲区的长度,如果nbytes > 套接字sockfd的发送缓冲区的长度, 该函数返回SOCKET_ERROR。

      2) 如果nbtyes <= 套接字sockfd的发送缓冲区的长度,那么send先检查协议是否正在发送sockfd的发送缓冲区中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲区中的数据或者sockfd的发送缓冲区中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和nbytes。

      3) 如果 nbytes > 套接字sockfd的发送缓冲区剩余空间的长度,send就一起等待协议把套接字sockfd的发送缓冲区中的数据发送完。

      4) 如果 nbytes < 套接字sockfd的发送缓冲区剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意:并不是send把套接字sockfd的发送缓冲区中的数据传到连接的另一端的,而是协议传送的。send仅仅是把buf中的数据copy到套接字sockfd的发送缓冲区的剩余空间里)。

      5) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。

      6) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。

      7) 在unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的处理是进程终止。



      ## 头文件

      lst_timer.h

      ```c++
      #ifndef LST_TIMER
      #define LST_TIMER

      #include <unistd.h>
      #include <signal.h>
      #include <sys/types.h>
      #include <sys/epoll.h>
      #include <fcntl.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      #include <assert.h>
      #include <sys/stat.h>
      #include <string.h>
      #include <pthread.h>
      #include <stdio.h>
      #include <stdlib.h>
      #include <sys/mman.h>
      #include <stdarg.h>
      #include <errno.h>
      #include <sys/wait.h>
      #include <sys/uio.h>

      #include <time.h>
      #include "../log/log.h"

      //连接资源结构体成员需要用到定时器类
      //前向声明
      class util_timer;

      //连接资源
      struct client_data
      {
      //客户端socket地址,项目中未使用
      sockaddr_in address;
      int sockfd;//socket文件描述符
      util_timer *timer;//定时器
      };
      //定时器类
      class util_timer
      {
      public:
      util_timer() : prev(NULL), next(NULL) {}

      public:
      time_t expire;//超时时间

      void (* cb_func)(client_data *);//回调函数指针,这个回调函数会删除client_data的资源连接
      client_data *user_data;//连接资源,嵌套类使用指针,相当于内部成员指针互相指向对方实例
      util_timer *prev;//前向定时器
      util_timer *next;//后继定时器
      };
      //定时器容器类,双向链表
      class sort_timer_lst
      {
      public:
      sort_timer_lst();
      ~sort_timer_lst();//常规销毁链表

      void add_timer(util_timer *timer);//添加定时器,内部调用私有成员add_timer
      void adjust_timer(util_timer *timer);//调整定时器,任务发生变化时,调整定时器在链表中的位置
      void del_timer(util_timer *timer);//删除定时器
      void tick();//定时任务处理函数,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。

      private:
      void add_timer(util_timer *timer, util_timer *lst_head);

      //创建头尾指针,方便管理
      util_timer *head;
      util_timer *tail;
      };

      class Utils//资源管理类
      {
      public:
      Utils() {}
      ~Utils() {}

      void init(int timeslot);

      //对文件描述符设置非阻塞
      int setnonblocking(int fd);

      //将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
      void addfd(int epollfd, int fd, bool one_shot, int TRIGMode);

      //信号处理函数
      static void sig_handler(int sig);

      //设置信号函数
      void addsig(int sig, void(handler)(int), bool restart = true);

      //定时处理任务,重新定时以不断触发SIGALRM信号
      void timer_handler();

      void show_error(int connfd, const char *info);

      public:
      static int *u_pipefd;//管道描述符
      sort_timer_lst m_timer_lst;//定时器容器
      static int u_epollfd;//事务描述符
      int m_TIMESLOT;
      };

      void cb_func(client_data *user_data);//回调函数

      #endif

.cpp实现

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
#include "lst_timer.h"
#include "../http/http_conn.h"

sort_timer_lst::sort_timer_lst()
{
head = NULL;
tail = NULL;
}
sort_timer_lst::~sort_timer_lst()//常规销毁链表
{
util_timer *tmp = head;
while (tmp)
{
head = tmp->next;
delete tmp;
tmp = head;
}
}

void sort_timer_lst::add_timer(util_timer *timer)
{
if (!timer)//没有timer要加
{
return;
}
if (!head)//链表中一个节点都没有
{
head = tail = timer;
return;
}

//如果新的定时器超时时间小于当前头部结点
//直接将当前定时器结点作为头部结点
if (timer->expire < head->expire)
{
timer->next = head;
head->prev = timer;
head = timer;
return;
}
//否则要插入链表中间或结尾,调用私有方法
add_timer(timer, head);
}

//调整定时器,任务发生变化时,调整定时器在链表中的位置
void sort_timer_lst::adjust_timer(util_timer *timer)
{
if (!timer)
{
return;
}
util_timer *tmp = timer->next;
//被调整的定时器在链表尾部,不调整
//定时器超时值仍然小于下一个定时器超时值,不调整(定时器刷新时间只可能更大,不用和前面的节点比较)
if (!tmp || (timer->expire < tmp->expire))
{
return;
}
//被调整定时器是链表头结点,将定时器取出,重新插入
if (timer == head)
{
head = head->next;
head->prev = NULL;
timer->next = NULL;
add_timer(timer, head);
}
//被调整定时器在内部,将定时器取出,重新插入
else
{
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
add_timer(timer, timer->next);//因为timer要比next大,所以next当头节点往后add就可以
}
}
//删除定时器
void sort_timer_lst::del_timer(util_timer *timer)
{
if (!timer)
{
return;
}
//链表中只有一个定时器,需要删除该定时器
if ((timer == head) && (timer == tail))
{
delete timer;
head = NULL;
tail = NULL;
return;
}
//被删除的定时器为头结点
if (timer == head)
{
head = head->next;
head->prev = NULL;
delete timer;
return;
}
//被删除的定时器为尾结点
if (timer == tail)
{
tail = tail->prev;
tail->next = NULL;
delete timer;
return;
}
//被删除的定时器在链表内部,常规链表结点删除
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
delete timer;
}

//定时任务处理函数,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。
void sort_timer_lst::tick()
{
if (!head)
{
return;
}
//获取当前时间,定时器的超时时间是以前设置的时间+n个单位超时时间(成为未来时间),所以用当前时间来比较判断是否超时
time_t cur = time(NULL);
util_timer *tmp = head;
//遍历定时器链表
while (tmp)
{
//链表容器为升序排列
//当前时间小于定时器的超时时间,后面的定时器也没有到期
if (cur < tmp->expire)
{
break;
}
//当前定时器到期,则调用回调函数,执行定时事件
tmp->cb_func(tmp->user_data);
//将处理后的定时器从链表容器中删除,并重置头结点
head = tmp->next;
if (head)
{
head->prev = NULL;
}
delete tmp;
tmp = head;
}
}

//私有成员,被公有成员add_timer和adjust_time调用
//主要用于调整链表内部结点
void sort_timer_lst::add_timer(util_timer *timer, util_timer *lst_head)
{
util_timer *prev = lst_head;
util_timer *tmp = prev->next;//不可能比头节点小,所以比较后面的一个节点
//遍历当前结点之后的链表,按照超时时间找到目标定时器对应的位置,常规双向链表插入操作
while (tmp)
{
if (timer->expire < tmp->expire)//可以插到tmp前面
{
prev->next = timer;
timer->next = tmp;
tmp->prev = timer;
timer->prev = prev;
break;
}
prev = tmp;
tmp = tmp->next;
}
//遍历完发现,目标定时器需要放到尾结点处
if (!tmp)
{
prev->next = timer;
timer->prev = prev;
timer->next = NULL;
tail = timer;
}
}

void Utils::init(int timeslot)
{
m_TIMESLOT = timeslot;//单位时间
}

//对文件描述符设置非阻塞
int Utils::setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}

//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
void Utils::addfd(int epollfd, int fd, bool one_shot, int TRIGMode)
{
epoll_event event;
event.data.fd = fd;

if (1 == TRIGMode)
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
else
event.events = EPOLLIN | EPOLLRDHUP;

if (one_shot)
event.events |= EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}

//信号处理函数
void Utils::sig_handler(int sig)
{
//为保证函数的可重入性,保留原来的errno
//可重入性表示中断后再次进入该函数,环境变量与之前相同,不会丢失数据
int save_errno = errno;
int msg = sig;
//将信号值从管道写端写入,传输字符类型,而非整型
send(u_pipefd[1], (char *)&msg, 1, 0);
errno = save_errno;
}

//设置信号函数,信号是指SIGALRM这些信号,本质是一个int
//当超时时(比如alarm)会产生这个信号,这里的设置(注册)就是让这个信号的处理按照这里设置的方式,比如flag和处理函数
void Utils::addsig(int sig, void(*handler)(int), bool restart)//handler是sig_handler
{
//创建sigaction结构体变量
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
//信号处理函数中仅仅发送信号值,不做对应逻辑处理
sa.sa_handler = handler;
if (restart)
sa.sa_flags |= SA_RESTART;
//将所有信号添加到信号集sa_mask中,屏蔽所有信号
sigfillset(&sa.sa_mask);
//执行sigaction函数
assert(sigaction(sig, &sa, NULL) != -1);//这个sig信号会屏蔽其他的信号
}

//定时处理任务,重新定时以不断触发SIGALRM信号
void Utils::timer_handler()
{
m_timer_lst.tick();
alarm(m_TIMESLOT);
}

void Utils::show_error(int connfd, const char *info)
{
send(connfd, info, strlen(info), 0);
close(connfd);
}

int *Utils::u_pipefd = 0;
int Utils::u_epollfd = 0;

class Utils;//在这声明是什么意思?

//定时器回调函数,tick函数调用
void cb_func(client_data *user_data)
{
//删除非活动连接在socket上的注册事件
epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
assert(user_data);
//关闭文件描述符
close(user_data->sockfd);
//减少连接数
http_conn::m_user_count--;
}

使用逻辑

首先注册(设置)好信号,比如SIGALRM信号,这使得产生这个信号时,能有对应的方式和处理(默认是终止进程),这里的方式是restart、屏蔽其他信号,处理是通过管道向主循环发这个信号。产生的方式是alarm(),主循环中每次尝试从管道获取信号,如果有这个信号,则设置timeout为true说明有超时事件要处理,因为是非必须事件,在这轮循环读写完再进行处理。

处理会调用timer_handler(),首先调用tick(),把定时器超时的都关了(调用cb_func),然后重新alarm()。

关于SIGTERM:程序结束信号,kill或Ctrl+C触发,默认终止进程。

也就是:

  • 信号

    • 1.先知道有些动作会产生一些信号

    • 2.设置(注册)这些信号产生后的动作——方式(flag)和处理函数(handler)

    • 3.处理函数只是通知主循环有个信号产生,主循环要做对应的处理。

  • 主循环,我们看看会发生什么

    • 1.当一个连接到来时,要创建一个定时器给它,初始化时间和回调函数等变量;
    • 2.如果连接有读写,更新时间;
    • 3.无论有没有定时器超时,每隔timeslot(时隙)会alarm一次,触发信号后主循环得知信号产生,去查看有哪些连接的定时器超时了,超时就关闭连接,然后重新设置alarm,循环往复。
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
//定时处理任务,重新定时以不断触发SIGALRM信号
void timer_handler()
{
timer_lst.tick();
alarm(TIMESLOT);
}

//创建定时器容器链表
static sort_timer_lst timer_lst;

//创建连接资源数组
client_data *users_timer = new client_data[MAX_FD];

//超时默认为False
bool timeout = false;

//alarm定时触发SIGALRM信号
alarm(TIMESLOT);

while (!stop_server)
{
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
break;
}

for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;

//处理新到的客户连接
if (sockfd == listenfd)
{
//初始化客户端连接地址
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);

//该连接分配的文件描述符
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);

//初始化该连接对应的连接资源
users_timer[connfd].address = client_address;
users_timer[connfd].sockfd = connfd;

//创建定时器临时变量
util_timer *timer = new util_timer;
//设置定时器对应的连接资源
timer->user_data = &users_timer[connfd];
//设置回调函数
timer->cb_func = cb_func;

time_t cur = time(NULL);
//设置绝对超时时间
timer->expire = cur + 3 * TIMESLOT;
//创建该连接对应的定时器,初始化为前述临时变量
users_timer[connfd].timer = timer;
//将该定时器添加到链表中
timer_lst.add_timer(timer);
}
//处理异常事件
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服务器端关闭连接,移除对应的定时器
cb_func(&users_timer[sockfd]);

util_timer *timer = users_timer[sockfd].timer;
if (timer)
{
timer_lst.del_timer(timer);
}
}

//处理定时器信号
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
//接收到SIGALRM信号,timeout设置为True
}

//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
//创建定时器临时变量,将该连接对应的定时器取出来
util_timer *timer = users_timer[sockfd].timer;
if (users[sockfd].read_once())
{
//若监测到读事件,将该事件放入请求队列
pool->append(users + sockfd);

//若有数据传输,则将定时器往后延迟3个单位
//对其在链表上的位置进行调整
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
timer_lst.adjust_timer(timer);
}
}
else
{
//服务器端关闭连接,移除对应的定时器
cb_func(&users_timer[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
else if (events[i].events & EPOLLOUT)
{
util_timer *timer = users_timer[sockfd].timer;
if (users[sockfd].write())
{
//若有数据传输,则将定时器往后延迟3个单位
//并对新的定时器在链表上的位置进行调整
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
timer_lst.adjust_timer(timer);
}
}
else
{
//服务器端关闭连接,移除对应的定时器
cb_func(&users_timer[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
}
//处理定时器为非必须事件,收到信号并不是立马处理
//完成读写事件后,再进行处理
if (timeout)
{
timer_handler();
timeout = false;
}
}

第六站

内容比较少

config

头文件config.h

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
#ifndef CONFIG_H
#define CONFIG_H

#include "webserver.h"

using namespace std;

class Config
{
public:
Config();
~Config(){};

void parse_arg(int argc, char*argv[]);

//端口号
int PORT;

//日志写入方式
int LOGWrite;

//触发组合模式
int TRIGMode;

//listenfd触发模式
int LISTENTrigmode;

//connfd触发模式
int CONNTrigmode;

//优雅关闭链接
int OPT_LINGER;

//数据库连接池数量
int sql_num;

//线程池内的线程数量
int thread_num;

//是否关闭日志
int close_log;

//并发模型选择
int actor_model;
};

#endif

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
#include "config.h"

Config::Config(){
//端口号,默认9006
PORT = 9006;

//日志写入方式,默认同步
LOGWrite = 0;

//触发组合模式,默认listenfd LT + connfd LT
TRIGMode = 0;

//listenfd触发模式,默认LT
LISTENTrigmode = 0;

//connfd触发模式,默认LT
CONNTrigmode = 0;

//优雅关闭链接,默认不使用
OPT_LINGER = 0;

//数据库连接池数量,默认8
sql_num = 8;

//线程池内的线程数量,默认8
thread_num = 8;

//关闭日志,默认不关闭
close_log = 0;

//并发模型,默认是proactor,这个后面介绍
actor_model = 0;
}

void Config::parse_arg(int argc, char*argv[]){//命令行形式获取参数
int opt;
const char *str = "p:l:m:o:s:t:c:a:";
while ((opt = getopt(argc, argv, str)) != -1)//这个函数下面介绍
{
switch (opt)//会重新排列参数顺序,所以要switch
{
case 'p':
{
PORT = atoi(optarg);
break;
}
case 'l':
{
LOGWrite = atoi(optarg);
break;
}
case 'm':
{
TRIGMode = atoi(optarg);
break;
}
case 'o':
{
OPT_LINGER = atoi(optarg);
break;
}
case 's':
{
sql_num = atoi(optarg);
break;
}
case 't':
{
thread_num = atoi(optarg);
break;
}
case 'c':
{
close_log = atoi(optarg);
break;
}
case 'a':
{
actor_model = atoi(optarg);
break;
}
default:
break;
}
}
}

  • getopt() 方法是用来分析命令行参数的,该方法由 Unix 标准库提供,包含在 <unistd.h> 头文件中。

  • ```c++
    int getopt(int argc, char * const argv[], const char *optstring);
    extern char *optarg; //选项的参数指针
    extern int optind, //下一次调用getopt的时,从optind存储的位置处重新开始检查选项。
    extern int opterr, //当opterr=0时,getopt不向stderr输出错误信息。
    extern int optopt; //当命令行选项字符不包括在optstring中或者选项缺少必要的参数时,该选项存储在optopt中,getopt返回’?’、

    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

    * argc:通常由函数直接传入,表示参数的数量

    * argv:通常也由函数直接传入,表示参数的字符串变量数组

    * optstring:一个包含正确的参数选项字符串,用于参数的解析。例如 “abc:”,其中 -a,-b 就表示两个普通选项,-c 表示一个必须有参数的选项,因为它后面有一个冒号

    * 1.单个字符,表示选项,
    * 2.单个字符后接一个冒号:表示该选项后必须跟一个参数。参数紧跟在选项后或者以空格隔开。该参数的指针赋给optarg。
    * 3 单个字符后跟两个冒号,表示该选项后必须跟一个参数。参数必须紧跟在选项后不能以空格隔开。该参数的指针赋给optarg。(这个特性是GNU的扩张)。

    * getopt处理以'-’开头的命令行参数,如optstring="ab:c::d::",命令行为getopt.exe -a -b host -ckeke -d haha
    在这个命令行参数中,-a和-h就是选项元素,去掉'-',a,b,c就是选项。host是b的参数,keke是c的参数。但haha并不是d的参数,因为它们中间有空格隔开。

    * getopt()用来分析命令行参数。参数argc和argv是由main()传递的参数个数和内容。参数optstring 则代表欲处理的选项字符串。此函数会返回在argv 中下一个的选项字母(指针不断移动),此字母会对应参数optstring 中的字母。如果选项字符串里的字母后接着冒号“:”,则表示还有相关的参数,全域变量optarg 即会指向此额外参数。如果getopt()找不到符合的参数则会印出错信息,并将全域变量optopt设为“?”字符,如果不希望getopt()印出错信息,则只要将全域变量opterr设为0即可。

    * 还要注意的是默认情况下getopt会重新排列命令行参数的顺序,所以到最后所有不包含选项的命令行参数都排到最后。
    如getopt.exe -a ima -b host -ckeke -d haha, 都最后命令行参数的顺序是: -a -b host -ckeke -d ima haha



    关于proactor模式,小林coding的这篇分析写得很好,推荐看一看:[如何深刻理解Reactor和Proactor? - 知乎 (zhihu.com)](https://www.zhihu.com/question/26943938)

    项目中用的是假reactor和模拟proactor(同步的):[(29条消息) 两种高效的事件处理模式:Reactor模式和Proactor模式_ZY-JIMMY的博客-CSDN博客_reactor模式和proactor](https://blog.csdn.net/ZYZMZM_/article/details/98049471)

    ## main

    main.cpp

    ```c++
    #include "config.h"

    int main(int argc, char *argv[])
    {
    //需要修改的数据库信息,登录名,密码,库名
    string user = "root";
    string passwd = "root";
    string databasename = "qgydb";

    //命令行解析
    Config config;
    config.parse_arg(argc, argv);

    WebServer server;//websever在config.h中导入了websever.h

    //初始化
    server.init(config.PORT, user, passwd, databasename, config.LOGWrite,
    config.OPT_LINGER, config.TRIGMode, config.sql_num, config.thread_num,
    config.close_log, config.actor_model);


    //日志
    server.log_write();

    //数据库
    server.sql_pool();

    //线程池
    server.thread_pool();

    //触发模式
    server.trig_mode();

    //监听
    server.eventListen();

    //运行
    server.eventLoop();

    return 0;
    }

makefile&g++

先看这个入门:(29条消息) Makefile 语法入门_阿飞__的博客-CSDN博客_makefile语法

再看:(29条消息) Makefile文件语法规则及用法总结_fangye945a的博客-CSDN博客_makefile语法规则

Makefile 条件判断 - ifeq、ifneq、ifdef、ifndef - Makefile 简明教程 | 宅学部落 (zhaixue.cc)

g++参数:C/C++专题—gcc g++ 参数详解 - 知乎 (zhihu.com)

rm命令:Linux rm命令:删除文件或目录 (biancheng.net)

项目中的makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CXX ?= g++

DEBUG ?= 1
ifeq ($(DEBUG), 1)
CXXFLAGS += -g
else
CXXFLAGS += -O2

endif

server: main.cpp ./timer/lst_timer.cpp ./http/http_conn.cpp ./log/log.cpp ./CGImysql/sql_connection_pool.cpp webserver.cpp config.cpp
$(CXX) -o server $^ $(CXXFLAGS) -lpthread -lmysqlclient

clean:
rm -r server

第七站

顶层实现

头文件websever.h

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
#ifndef WEBSERVER_H
#define WEBSERVER_H

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <cassert>
#include <sys/epoll.h>

#include "./threadpool/threadpool.h"
#include "./http/http_conn.h"

const int MAX_FD = 65536; //最大文件描述符,即最大连接数
const int MAX_EVENT_NUMBER = 10000; //最大事件数
const int TIMESLOT = 5; //最小超时单位

class WebServer
{
public:
WebServer();
~WebServer();

void init(int port , string user, string passWord, string databaseName,
int log_write , int opt_linger, int trigmode, int sql_num,
int thread_num, int close_log, int actor_model);

void thread_pool();
void sql_pool();
void log_write();
void trig_mode();
void eventListen();
void eventLoop();
void timer(int connfd, struct sockaddr_in client_address);
void adjust_timer(util_timer *timer);
void deal_timer(util_timer *timer, int sockfd);
bool dealclinetdata();
bool dealwithsignal(bool& timeout, bool& stop_server);
void dealwithread(int sockfd);
void dealwithwrite(int sockfd);

public:
//基础
int m_port;//端口
char *m_root;//根目录地址
int m_log_write;//是否要异步写日志,异步写用一个阻塞队列
int m_close_log;//是否关闭日志
int m_actormodel;//模型切换

int m_pipefd[2];//管道通信
int m_epollfd;//内核描述符
http_conn *users;

//数据库相关
connection_pool *m_connPool;
string m_user; //登陆数据库用户名
string m_passWord; //登陆数据库密码
string m_databaseName; //使用数据库名
int m_sql_num;

//线程池相关
threadpool<http_conn> *m_pool;
int m_thread_num;

//epoll_event相关
epoll_event events[MAX_EVENT_NUMBER];

int m_listenfd;//监听文件描述符
int m_OPT_LINGER;
int m_TRIGMode;//控制连接和读写的触发模式
int m_LISTENTrigmode;
int m_CONNTrigmode;

//定时器相关
client_data *users_timer;
Utils utils;
};
#endif

.cpp实现

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
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
#include "webserver.h"

WebServer::WebServer()
{
//http_conn类对象
users = new http_conn[MAX_FD];

//root文件夹路径
char server_path[200];
getcwd(server_path, 200);
char root[6] = "/root";
m_root = (char *)malloc(strlen(server_path) + strlen(root) + 1);
strcpy(m_root, server_path);
strcat(m_root, root);

//定时器
users_timer = new client_data[MAX_FD];//每个连接都对应一个定时器
}

WebServer::~WebServer()
{
close(m_epollfd);
close(m_listenfd);
close(m_pipefd[1]);
close(m_pipefd[0]);
delete[] users;
delete[] users_timer;
delete m_pool;
}

void WebServer::init(int port, string user, string passWord, string databaseName, int log_write,
int opt_linger, int trigmode, int sql_num, int thread_num, int close_log, int actor_model)
{
m_port = port;
m_user = user;
m_passWord = passWord;
m_databaseName = databaseName;
m_sql_num = sql_num;
m_thread_num = thread_num;
m_log_write = log_write;
m_OPT_LINGER = opt_linger;
m_TRIGMode = trigmode;
m_close_log = close_log;
m_actormodel = actor_model;
}

void WebServer::trig_mode()
{
//LT + LT
if (0 == m_TRIGMode)
{
m_LISTENTrigmode = 0;//读写的模式
m_CONNTrigmode = 0;//连接的模式
}
//LT + ET
else if (1 == m_TRIGMode)
{
m_LISTENTrigmode = 0;
m_CONNTrigmode = 1;
}
//ET + LT
else if (2 == m_TRIGMode)
{
m_LISTENTrigmode = 1;
m_CONNTrigmode = 0;
}
//ET + ET
else if (3 == m_TRIGMode)
{
m_LISTENTrigmode = 1;
m_CONNTrigmode = 1;
}
}

void WebServer::log_write()
{
if (0 == m_close_log)
{
//初始化日志
if (1 == m_log_write)//是否异步写日志
Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 800);//800是阻塞队列长度
else
Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);
}
}

void WebServer::sql_pool()
{
//初始化数据库连接池
m_connPool = connection_pool::GetInstance();
m_connPool->init("localhost", m_user, m_passWord, m_databaseName, 3306, m_sql_num, m_close_log);

//初始化数据库读取表
users->initmysql_result(m_connPool);
}

void WebServer::thread_pool()
{
//线程池,前面创建了users一组http_conn对象,每个对象的工作处理由线程池调用
m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);//只有这一个实例
}

void WebServer::eventListen()
{
//网络编程基础步骤
m_listenfd = socket(PF_INET, SOCK_STREAM, 0);//创建一个套接字,监听套接口,socket函数看后面
assert(m_listenfd >= 0);

//优雅关闭连接
if (0 == m_OPT_LINGER)
{
struct linger tmp = {0, 1};
setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
}
else if (1 == m_OPT_LINGER)
{
struct linger tmp = {1, 1};
setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
}

int ret = 0;
//地址配置
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);//本函数将一个32位数从主机字节顺序转换成网络字节顺序。INADDR_ANY见后面
address.sin_port = htons(m_port);//将整型变量从主机字节顺序转变成网络字节顺序,就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
/*
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,
从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用big-endian排序方式。
*/
int flag = 1;
//打开地址复用功能,允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接
setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));//见后面
assert(ret >= 0);
ret = listen(m_listenfd, 5);
assert(ret >= 0);

utils.init(TIMESLOT);//初始化资源管理类

//epoll创建内核事件表
epoll_event events[MAX_EVENT_NUMBER];//事件集合
m_epollfd = epoll_create(5);//事件表描述符
assert(m_epollfd != -1);

utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);//内核事件表注册读事件,监听socket
http_conn::m_epollfd = m_epollfd;

ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);//创建管道套接字
assert(ret != -1);
utils.setnonblocking(m_pipefd[1]);//信号写端,设置非阻塞,当缓冲区满了时候不阻塞,减少send执行时间
utils.addfd(m_epollfd, m_pipefd[0], false, 0);//设置管道读端为ET非阻塞

//传递给主循环的信号值,这里为信号注册处理函数,restart是false,在程序中手动重新设置
utils.addsig(SIGPIPE, SIG_IGN);//忽略连接断开信号
utils.addsig(SIGALRM, utils.sig_handler, false);//超时信号
utils.addsig(SIGTERM, utils.sig_handler, false);//终止信号

alarm(TIMESLOT);//开始计时

//工具类,信号和描述符基础操作
Utils::u_pipefd = m_pipefd;
Utils::u_epollfd = m_epollfd;
}

void WebServer::timer(int connfd, struct sockaddr_in client_address)//获取一个连接后,初始化计时器和用户数据
{
users[connfd].init(connfd, client_address, m_root, m_CONNTrigmode, m_close_log, m_user, m_passWord, m_databaseName);

//初始化client_data数据
//创建定时器,设置回调函数和超时时间,绑定用户数据,将定时器添加到链表中
users_timer[connfd].address = client_address;
users_timer[connfd].sockfd = connfd;
util_timer *timer = new util_timer;
timer->user_data = &users_timer[connfd];
timer->cb_func = cb_func;
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
users_timer[connfd].timer = timer;
utils.m_timer_lst.add_timer(timer);
}

//若有数据传输,则将定时器往后延迟3个单位
//并对新的定时器在链表上的位置进行调整
void WebServer::adjust_timer(util_timer *timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
utils.m_timer_lst.adjust_timer(timer);

LOG_INFO("%s", "adjust timer once");
}

void WebServer::deal_timer(util_timer *timer, int sockfd)
{
timer->cb_func(&users_timer[sockfd]);
if (timer)
{
utils.m_timer_lst.del_timer(timer);
}

LOG_INFO("close fd %d", users_timer[sockfd].sockfd);
}

bool WebServer::dealclinetdata()
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
if (0 == m_LISTENTrigmode)//LT模式
{
int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);//返回值是连接描述符
if (connfd < 0)
{
LOG_ERROR("%s:errno is:%d", "accept error", errno);
return false;
}
if (http_conn::m_user_count >= MAX_FD)
{
utils.show_error(connfd, "Internal server busy");
LOG_ERROR("%s", "Internal server busy");
return false;
}
timer(connfd, client_address);
}

else//ET模式
{
while (1)//必须一次把监听到的连接读取完,因此循环读取、初始化,直至缓冲区为空
{
int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
LOG_ERROR("%s:errno is:%d", "accept error", errno);
break;
}
if (http_conn::m_user_count >= MAX_FD)
{
utils.show_error(connfd, "Internal server busy");
LOG_ERROR("%s", "Internal server busy");
break;
}
timer(connfd, client_address);//读取一个连接就初始化
}
return false;
}
return true;
}

bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{
int ret = 0;
int sig;
char signals[1024];
ret = recv(m_pipefd[0], signals, sizeof(signals), 0);//从读管道读取信号,放到signals缓冲区
//recv的作用,就是通过fdt找到这个缓冲区,并把数据复制到咱们的参数2指向的地址,复制参数3个
//返回:读出来的字节大小;客户端下线,返回0;执行失败,返回-1
//正常情况下,这里的ret返回值总是1,只有14和15两个ASCII码对应的字符
if (ret == -1)
{
return false;
}
else if (ret == 0)
{
return false;
}
else
{
for (int i = 0; i < ret; ++i)
{
switch (signals[i])
{
case SIGALRM:
{
timeout = true;
break;
}
case SIGTERM:
{
stop_server = true;
break;
}
}
}
}
return true;
}

void WebServer::dealwithread(int sockfd)
{
util_timer *timer = users_timer[sockfd].timer;

//reactor,非阻塞同步
if (1 == m_actormodel)
{
if (timer)
{
adjust_timer(timer);
}

//若监测到读事件,将该事件放入请求队列(线程池的请求队列中),同步模式让线程处理整个读过程和相响应报文生成过程
m_pool->append(users + sockfd, 0);//users[sockfd]

while (true)//一直等待这个读事件完成,很多评论说是作者偷懒了,
//这不是reactor模式,这相当于阻塞了,最多只有一个http请求。完全没有发挥线程池的作用
{
if (1 == users[sockfd].improv)//完成标志
{
if (1 == users[sockfd].timer_flag)//如果要关闭的话
{
deal_timer(timer, sockfd);//关闭连接
users[sockfd].timer_flag = 0;
}
users[sockfd].improv = 0;
break;
}
}
}
else//异步,主线程和内核执行IO,工作线程负责业务处理
{
//proactor,实际上这里是模拟proactor模式,是同步的模式,只有主线程串行IO
if (users[sockfd].read_once())//读数据成功的话(无论是LT还是ET),返回true
{
LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));

//若监测到读事件,将该事件放入请求队列
m_pool->append_p(users + sockfd);

if (timer)
{
adjust_timer(timer);
}
}
else
{
deal_timer(timer, sockfd);
}
}
}

void WebServer::dealwithwrite(int sockfd)
{
util_timer *timer = users_timer[sockfd].timer;
//reactor
if (1 == m_actormodel)
{
if (timer)
{
adjust_timer(timer);
}

m_pool->append(users + sockfd, 1);

while (true)
{
if (1 == users[sockfd].improv)
{
if (1 == users[sockfd].timer_flag)
{
deal_timer(timer, sockfd);
users[sockfd].timer_flag = 0;
}
users[sockfd].improv = 0;
break;
}
}
}
else
{
//proactor
if (users[sockfd].write())
{
LOG_INFO("send data to the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));

if (timer)
{
adjust_timer(timer);
}
}
else
{
deal_timer(timer, sockfd);
}
}
}

void WebServer::eventLoop()//主循环
{
bool timeout = false;
bool stop_server = false;

while (!stop_server)
{
int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
LOG_ERROR("%s", "epoll failure");
break;
}

for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;

//处理新到的客户连接
if (sockfd == m_listenfd)
{
bool flag = dealclinetdata();
if (false == flag)
continue;
}
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服务器端关闭连接,移除对应的定时器
util_timer *timer = users_timer[sockfd].timer;
deal_timer(timer, sockfd);
}
//处理信号
else if ((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN))
{
bool flag = dealwithsignal(timeout, stop_server);
if (false == flag)
LOG_ERROR("%s", "dealclientdata failure");
}
//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
dealwithread(sockfd);
}
else if (events[i].events & EPOLLOUT)
{
dealwithwrite(sockfd);
}
}
if (timeout)
{
utils.timer_handler();

LOG_INFO("%s", "timer tick");

timeout = false;
}
}
}

  • int socket(int af, int type, int protocol);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    * af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
    * type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。
    * protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
    * 为什么还需要第三个参数呢?一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

    * 若无错误发生,socket()返回引用新套接口的描述字。



    * ```c++
    int listen(int sockfd, int backlog);
  • listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

  • 成功返回0

  • int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
    1
    2
    3
    4
    5

    * connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

    * ```c++
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

  • accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为客户端协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。


  • struct linger用法:

  • Linux下tcp连接断开的时候调用close()函数,有优雅断开和强制断开两种方式。那么如何设置断开连接的方式呢?是通过设置socket描述符一个linger结构体属性。

  • ```c++
    #include <arpa/inet.h>

    struct linger {
      int l_onoff;
      int l_linger;
    };

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    * 三种断开方式:

    * 1. l_onoff = 0; l_linger忽略:close()立刻返回,底层会将未发送完的数据发送完成后再释放资源,即优雅退出。
    2. l_onoff != 0; l_linger = 0:close()立刻返回,但不会发送未发送完成的数据,而是通过一个REST包强制的关闭socket描述符,即强制退出。
    3. l_onoff != 0; l_linger > 0:close()不会立刻返回,内核会延迟一段时间,这个时间就由l_linger的值来决定。如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,close()会返回正确,socket描述符优雅性退出。否则,close()会直接返回错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的时,如果socket描述符被设置为非堵塞型,则close()会直接返回值。

    -----

    使用完linger之后,就用setsockopt()设置

    ```c++
    #include <sys/socket.h>

    int setsockopt( int socket, int level, int option_name,const void *option_value, size_t ,ption_len);

第一个参数socket是套接字描述符。第二个参数level是被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET。 option_name指定准备设置的选项,len是选项的长度。option_name可以有哪些取值,这取决于level,以linux 2.6内核为例(在不同的平台上,这种关系可能会有不同),在套接字级别上(SOL_SOCKET),option_name可以有以下取值:

  • SO_LINGER,如果选择此选项, close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回。该选项的参数(option_value)是一个linger结构:

    1
    2
    3
    4
    5
    6
    7
    8
    struct linger {
    int l_onoff;
    int l_linger;
    };
    /*
    如果linger.l_onoff值为0(关闭),则清 sock->sk->sk_flag中的SOCK_LINGER位;
    否则,置该位,并赋sk->sk_lingertime值为 linger.l_linger。
    */
  • SO_DEBUG,打开或关闭调试信息。当option_value不等于0时,打开调试信息,否则,关闭调试信息。它实际所做的工作是在sock->sk->sk_flag中置 SOCK_DBG(第10)位,或清SOCK_DBG位。

  • SO_REUSEADDR,打开或关闭地址复用功能。当option_value不等于0时,打开,否则,关闭。它实际所做的工作是置sock->sk->sk_reuse为1或0。

    • SO_REUSEADDR是一个很有用的选项,一般服务器的监听socket都应该打开它。它的大意是允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接,比如:
      • 服务器启动后,有客户端连接并已建立,如果服务器主动关闭,那么和客户端的连接会处于TIME_WAIT状态,此时再次启动服务器,就会bind不成功,报:Address already in use。

      • 服务器父进程监听客户端,当和客户端建立链接后,fork一个子进程专门处理客户端的请求,如果父进程停止,因为子进程还和客户端有连接,所以再次启动父进程,也会报Address already in use。

  • SO_DONTROUTE,打开或关闭路由查找功能。当option_value不等于0时,打开,否则,关闭。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_LOCALROUTE位。

  • SO_BROADCAST,允许或禁止发送广播数据。当option_value不等于0时,允许,否则,禁止。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_BROADCAST位。

  • 等等…太多了。


  • sockaddr_in

  • struct sockaddr_in {
        __uint8_t sin_len;
        sa_family_t sin_family;
        in_port_t sin_port;
        struct in_addr sin_addr;
        char sin_zero[8];
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    * sin_family指代协议族,在socket编程中只能是AF_INET
    * sin_port存储端口号(使用网络字节顺序)
    * sin_addr存储IP地址,使用in_addr这个数据结构
    * sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
    * addr.sin_len=sizeof(addr);//socket字节长度

    * sockaddr_in 结构体:struct sockaddr_in中的in 表示internet,就是网络地址,这只是我们比较常用的地址结构,属于AF_INET地址族,非常地常用

    * sin_zero 初始值应该使用函数 bzero() 来全部置零。一般采用下面语句

    * ```c++
    struct sockaddr_in cliaddr;
    bzero(&cliaddr,sizeof(cliaddr));
  • sockaddr_in结构体变量的基本配置

    • ```c++
      struct sockaddr_in ina;
      bzero(&ina,sizeof(ina));
      ina.sin_family=AF_INET;
      ina.sin_port=htons(23);
      ina.sin_addr.s_addr = inet_addr(“132.241.5.10”);
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10



      * sockaddr

      * ```c++
      struct sockaddr {
      unsigned short sa_family; /* address family, AF_xxx */
      char sa_data[14]; /* 14字节,包含目标地址和端口信息 */
      };
  • sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了

  • sockaddr_in和sockaddr二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。

  • sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。

  • sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。


  • INADDR_ANY
  • 转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。
    比如一台电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某个端口,那他要监听哪个网卡地址的端口呢?
  • 如果绑定某个具体的ip地址,你只能监听你所设置的ip地址所在的网卡的端口,其它两块网卡无法监听端口,如果我需要三个网卡都监听,那就需要绑定3个ip,也就等于需要管理3个套接字进行数据交换,这样岂不是很繁琐?
  • 所以出现INADDR_ANY,你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。

  • bind:服务端用于将把用于通信的地址和端口绑定到 socket上。

  • int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
    
    • 参数 sockfd ,需要绑定的socket。
    • 参数 addr ,存放了服务端用于通信的地址和端口。ip地址和端口号是放在 socketaddr_in 结构体里面的。
    • 参数 addrlen ,表示 addr 结构体的大小。
  • 返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中。如果绑定的地址错误,或者端口已被占用,bind 函数一定会报错,否则一般不会返回错误

压力测试

先安装依赖

image-20220919164341581

然后到webbench-1.5的目录下:make clean,再make。

测试:

image-20220919164437394

day1

数组中重复数字

很简单的一道题,用哈希映射可以做出来,需要额外空间,另一种解法比较难想,是“原地交换”的方法。

1
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
1
2
3
4
5
示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
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
//原地交换
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
int n = nums.size();
int i=0;
while(i<n)
{
if(nums[i]==i)
{
i++;
continue;
}
if(nums[nums[i]]==nums[i])
return nums[i];
swap(nums[nums[i]],nums[i]);
}


return -1;

}
};
/*
执行用时:24 ms, 在所有 C++ 提交中击败了96.40%的用户
内存消耗:22.4 MB, 在所有 C++ 提交中击败了66.33%的用户
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//映射
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
const int n = nums.size();
int array[n];//哈希表
for(int i=0;i<n;i++)
array[i]=-1;//初始化
for(int i=0;i<n;i++)
if(array[nums[i]]==-1)
array[nums[i]]=1;//标记
else
return nums[i];
return -1;
}
};
/*
执行用时: 32 ms
内存消耗: 22.9 MB
*/

二维数组中的查找

蛮简单的,但是有些细节需要注意。从左下角看上去就类似是一个二叉搜索树。按照这个性质,从左下角开始比较,目标元素小就往上找,大就往右找,每次都能消去一行。

1
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
示例:

现有矩阵 matrix 如下:

[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。

给定 target = 20,返回 false。
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
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
//注意,必须先判断大小即数组合不合法,因为如果n=0.说明是空数组,这样取m就是错误的了,因为根本没有matrix[0]这个元素
int n = matrix.size();
if(n<=0) return false;
int m = matrix[0].size();
if(m<=0) return false;
//查找
int i=n-1,j=0;
while(i>=0&&j<=m-1)
{
if(target==matrix[i][j])
return true;
else if(target<matrix[i][j])
i--;
else
j++;

}
return false;
}
};
/*
执行用时:20 ms, 在所有 C++ 提交中击败了79.82%的用户
内存消耗:12.7 MB, 在所有 C++ 提交中击败了51.61%的用户
*/

//实际上上面的if判断有冗余,可以利用bool表达式的形式来简化代码
class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
int n = matrix.size();
int i=n-1,j=0;
while(i>=0 && j<matrix[0].size())//一旦i<0说明n<=0,此时已经是false,不会判断后面的j,也就不会取matrix[0]
//这样一个n的if在while里判断了,一个m的if省略掉了
//不能写成j<=matrix[0].size()-1,不造为啥,力扣编译器的问题?
{
if(target==matrix[i][j])
return true;
else if(target<matrix[i][j])
i--;
else
j++;

}
return false;
}
};
/*
执行用时:16 ms, 在所有 C++ 提交中击败了97.12%的用户
内存消耗:12.8 MB, 在所有 C++ 提交中击败了5.28%的用户
*/

替换空格

这题主要是对string要有了解。首先需要一个更长的sting,这是我们替换字符(字符数变多)的前提。string可以原地腾出空间,即用resize弄出空位,这给了一个不用额外多一个辅助空间的条件。

所以首先要算出长度,即先遍历一遍sting看空格数,然后resize。

接着重点是,两个指针从尾向前遍历、替换。正是因为从后往前才不会影响到原有的元素(对尾部操作是由于尾部都是空的)。并且从后往前,当两个指针位置相等时就可以停止,因为不可能再替换。

1
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
1
2
3
4
示例 1:

输入:s = "We are happy."
输出:"We%20are%20happy."
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
class Solution {
public:
string replaceSpace(string s) {
int count = 0, len = s.size();
//计数
for(int i=0;i<len;i++)
if(s[i]==' ')
count++;
//变换大小,变换后是替换后的大小,尾部那些是空位
s.resize(len+count*2);

//从两个尾部开始,一个是原先的尾部,一个是现在的尾部
int i = len-1, j = len+count*2-1;
while(i<j)
{
if(s[i]!=' ')//不用换
{
s[j]=s[i];
i--;
j--;//减了刚好进行下一个if判断
}
if(s[i]==' ')//要替换
{
s[j]='0';
s[j-1]='2';
s[j-2]='%';
j-=3;
i--;
}
}
return s;
}
};

/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:6 MB, 在所有 C++ 提交中击败了83.37%的用户
*/

day2

从尾到头打印链表

因为是从尾到头,有种先进后出的意思,那么可以用一个辅助栈来存储。如果不允许额外的空间,则可以先反转链表,再顺序取出。

1
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
1
2
3
4
示例 1:

输入:head = [1,3,2]
输出:[2,3,1]
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
//辅助栈
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> vec;
stack<int> s;
ListNode* cur = head;
while(cur)
{
s.push(cur->val);
cur=cur->next;
}
while(!s.empty())
{
vec.push_back(s.top());
s.pop();
}
return vec;
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了73.73%的用户
内存消耗:8.6 MB, 在所有 C++ 提交中击败了35.04%的用户
*/

//反转链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> vec;

ListNode* cur = head;//当前指针
ListNode* pre = nullptr;//前向指针,注意head的next变为NULL,故pre初始化为nullptr
while(cur)
{
ListNode* tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
while(pre)
{
vec.push_back(pre->val);
pre = pre->next;
}
return vec;
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了73.73%的用户
内存消耗:8.3 MB, 在所有 C++ 提交中击败了93.13%的用户
*/

用两个栈实现队列

将栈分为一个主栈一个辅助栈,这样就能将插入和删除更具有目的性,因此插入就很简单,直接push主栈里,而不去考虑位置,这个问题留到删除来解决。

对于删除,主要是要删头部也就是第一个进来的,但在栈中它位于底部。因此要把主栈元素都倒出来,放辅助栈里,这样辅助栈就是一个按顺序的队列。因此:如果辅助栈不是空的,说明它被倒进来了,顶部元素就是第一个元素,删掉它;如果辅助栈是空的,则元素都在主栈里,如果主栈是空的,返回-1;如果主栈不是空的,就需要把元素倒出来给辅助栈,然后删顶部元素。

1
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
1
2
3
4
5
6
示例 1:

输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]
1
2
3
4
5
6
示例 2:

输入:
["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]
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
class CQueue {
public:
CQueue() {

}

void appendTail(int value) {
smain.push(value);
}

int deleteHead() {
if(!shelp.empty())
{
int x=shelp.top();
shelp.pop();
return x;
}
if(smain.empty())
return -1;

while(!smain.empty())
{
shelp.push(smain.top());
smain.pop();
}
int x = shelp.top();
shelp.pop();
return x;
}
private:
stack<int> smain;//主栈
stack<int> shelp;//辅助栈
};
/*
执行用时:248 ms, 在所有 C++ 提交中击败了76.96%的用户
内存消耗:101 MB, 在所有 C++ 提交中击败了72.52%的用户
*/

day3

斐波那契数列

简单的动态规划,注意要在运算过程中取模,不然会越界。

1
2
3
4
5
6
7
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
1
2
3
4
5
6
7
8
示例 1:

输入:n = 2
输出:1
示例 2:

输入:n = 5
输出:5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int fib(int n) {
if(n<2) return n;
int max = 1000000007;
int a=1,b=0;//从f1和f0开始
while(--n)
{
int tmp = (a+b)%max;//tmp相当于fn
b=a;//fn-2向前变成fn-1
a=tmp;//fn-1向前变成fn
}
return a;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.7 MB, 在所有 C++ 提交中击败了86.1%的用户
*/

青蛙跳台阶问题

一次跳1或2,则到第n个台阶为从n-1或从n-2;因此f(n)=f(n-1)+f(n-2),本质也是斐波那契问题,用动态规划。不同的是初值不同,f(0)=f(1)=1。

1
2
3
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
1
2
3
4
5
6
7
8
9
10
11
12
示例 1:

输入:n = 2
输出:2
示例 2:

输入:n = 7
输出:21
示例 3:

输入:n = 0
输出:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int numWays(int n) {
if(n==0) return 1;
if(n<=2) return n;
int max = 1000000007;
int a=1,b=1;//从f1和f0开始
while(--n)
{
int tmp = (a+b)%max;//tmp相当于fn
b=a;//fn-2向前变成fn-1
a=tmp;//fn-1向前变成fn
}
return a;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.7 MB, 在所有 C++ 提交中击败了93.27%的用户
*/

旋转数组的最小数字

这题的数组在某种程度上是有序的,因此用二分。但二分是用中间值和target比较,这里的target在哪呢?就用两个端点值,而且只能用右端点的值,这样才能缩小区间。为何要用右端点呢,是因为右端点是截断点,根据比较能缩小区间,左端点则不行。原则上将数组分成两个有序数组(左边段和右边段),就可以缩小区间了。

在比较之后,如果中间大于右端点,说明中间在左边段,而最小值一定在右边段,即在low之后,因此可以缩小区间,把low变成mid,而此时mid不可能是最小值,因此可以变成mid+1。并且必须这样,不然假如是5,1,那么low=mid,一直死循环。

如果中间小于右端点,则中间点在右边段了,最小值位置在mid及mid以前,缩小区间high=mid,因为mid也可能是最小值,因此high不能mid-1。

如果中间和右端点相等,这是因为这里的数组元素可以相等,此时high不能直接=mid,因为左边段可以和右边段相等,如3,3,1,3。直接相等就越过了1。也不能让low=mid,因为右段也可以和右端点相等。1,3,3的情况下,就越过了1。因此,直接将high-1就可以了,这样可以保证不越过又可以慢慢缩小区间,如果high是最小值,那么mid也是最小值,不会越过。

1
2
3
4
5
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为 1。

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
1
2
3
4
5
6
7
8
示例 1:

输入:numbers = [3,4,5,1,2]
输出:1
示例 2:

输入:numbers = [2,2,2,0,1]
输出:0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int minArray(vector<int>& numbers) {
int low = 0, high = numbers.size()-1;
while(low<high)
{
int mid = low + (high-low)/2;
if(numbers[mid]<numbers[high]) high = mid;
else if(numbers[mid]>numbers[high]) low = mid+1;
else high-=1;
}
return numbers[low];
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了86.65%的用户
内存消耗:11.7 MB, 在所有 C++ 提交中击败了71.43%的用户
*/

矩阵中的路径

这道题找路径的,就可以用深度优先搜索(dfs),由于每个元素都可以当开头,因此要对每个元素都用一次dfs。然后考虑一下剪枝,在dfs的过程中每个位置都可以继续向上下左右出发(用逻辑或连接起来),因此第一个要考虑的部分就是越界问题;其次,每次dfs都向后探一个字符串单词的字母,如果正确才能继续,因此第二个要考虑的就是当这个位置的字母不正确就返回false。这样就可以保证找到所有的可能。

还要考虑标记的问题,因为矩阵的元素不能重复使用,当这个元素正确要向后dfs时,必须先把这个元素标记不可用,在c++中用’\0’就可以。在dfs之后,还要标记回来。

1
2
3
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
1
2
3
4
5
6
7
8
示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:

输入:board = [["a","b"],["c","d"]], word = "abcd"
输出:false
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
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
n = board.size();
m = board[0].size();//初始化行列
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
if(dfs(board,word,i,j,0))//对每个位置都dfs,0表示单词开始,如果找到则直接返回true
return true;
return false;
}
private:
int n, m;//声明行列
bool dfs(vector<vector<char>>& board, string word, int i, int j, int k)
{

if(i<0||j<0||i>=n||j>=m||board[i][j]!=word[k]) return false;//越界或不对应则剪枝
if(k==word.size()-1) return true;//没越界且对应,长度也对应,成功
//标记
board[i][j] = '\0';
//向上下左右出发,k+1
bool res = dfs(board, word, i+1, j, k+1)||dfs(board, word, i, j+1, k+1)||
dfs(board, word, i-1, j, k+1)||dfs(board, word, i, j-1, k+1);
board[i][j] = word[k];//标记回来
return res;
}
};
/*
执行用时:520 ms, 在所有 C++ 提交中击败了18.67%的用户
内存消耗:5.9 MB, 在所有 C++ 提交中击败了98.27%的用户
*/

day4

I-剪绳子

这种题首先是求最大值,然后因为乘积是可分解的,因此这个问题可以缩小规模,就可以考虑用动态规划,实际上有更简单的数学解法。对于动态规划,n是从2开始的,然后当长度是n的时候,可以将乘积分两段,要么直接乘,要么对前一段再分(也就是小规模的再动态规划),后一段的长度通过遍历解决,就不用动态规划了。因此长度n时有两种选择,两段乘或再分,然后还要继续遍历,因此要保存好前面求的最大值。

1
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]\*k[1]\*...*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
1
2
3
4
5
6
7
8
9
10
示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int cuttingRope(int n) {
vector<int> dp(n+1);
dp[2] = 1;//初始化
for(int i=3;i<=n;i++)//i从3开始,到n
for(int j=1;j<=i-1;j++)//j从1开始,长度到i-1(最简单的遍历方式),
//这里j从2开始也可以,长度到i-2也可以,因为长度为1的划分没有意义
//但不能同时,要确保能进入循环,才有dp的定义
dp[i] = max(dp[i],max((i-j)*j,dp[i-j]*j));//首先要和之前遍历出来的dp[i]比较。然后看
//是直接乘更大还是继续划分,j不用dp,因为是遍历的
return dp[n];
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:6 MB, 在所有 C++ 提交中击败了63.63%的用户
*/

day5

II-剪绳子

这题就不方便用动态规划了,因为会溢出。这个问题出自我们是对结果取余,用动态规划max比较时,取余会造成max比较不正确,比如一个大的取余反而小了。因此不能在比较时候取余,那么在计算过程中就会溢出,即使用long long int也存在这个问题。

那么就可以用到数学的解法,因为数学的解法不需要比较,只需要一直运算就可以:

  • 根据几何不等式,等分时乘积最大;

  • 等分为长x的a段有:ax=n,则乘积为$x^a$,由于 n 为常数,因此当 $x^{\frac{1}{x}}$ 取最大值时, 乘积达到最大值。因为$x^a=x^{\frac{n}{x}}$

  • 因此对$x^{\frac{1}{x}}$求极大值,取对数有lny = lnx/x,求导得x=e。那么x可取2或3,代入一下2和3,同时取6次方发现3^2=9大一些,因此最好分成长为3的。

结论:

最优: 3 。把绳子尽可能切为多个长度为 3 的片段,留下的最后一段绳子的长度可能为 0,1,2 三种情况。
次优: 2 。若最后一段绳子长度为 2 ;则保留,不再拆为 1+1 。
最差: 1 。若最后一段绳子长度为 1 ;则应把一份 3 + 1 替换为 2 + 2,因为 $2 \times 2 > 3 \times 1$。

1
2
3
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m - 1] 。请问 k[0]\*k[1]\*...*k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
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
class Solution {
public:
int cuttingRope(int n) {
if (n <= 3) return n - 1;//必须切分一次
long ret = 1;
if (n % 3 == 1){
ret = 4;//最后的4变成2*2
n = n - 4;
}
if (n % 3 == 2){
ret = 2;//最后的2留着
n = n - 2;
}
while (n) {
ret = ret * 3 % 1000000007;//这里可以取模的原因是,跟max不同,ret是已经确定好的答案,只是一直没算完,
//先模后模的结果是一样的
n = n - 3;
}
return (int)ret;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.7 MB, 在所有 C++ 提交中击败了86.22%的用户
*/

二进制中1的个数

第一种方法是逐位看是不是1,直接把1左移然后和n与运算那就好。也可以模2来做。

第二种方法是用n&(n-1),因为n-1会把第一个1右边的0变成1,且这个1变成0,那么再与n做与运算时,实际上就是把n的第一个1消去了(原来的0和1&也是0,但原来的1由于变成了0,&后也是0),因此每做一次这个操作,就有一个1。

1
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为 [汉明重量]。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
示例 1:

输入:n = 11 (控制台输入 00000000000000000000000000001011)
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:

输入:n = 128 (控制台输入 00000000000000000000000010000000)
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:

输入:n = 4294967293 (控制台输入 11111111111111111111111111111101,部分语言中 n = -3)
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//逐位比较
class Solution {
public:
int hammingWeight(uint32_t n) {
int res = 0;
for(int i=0;i<32;i++)
if(n&(1<<i))//不断将1左移i位,也就是和n的第i位对齐,然后取与运算
res++;//如果结果非0,则是一个1
return res;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.9 MB, 在所有 C++ 提交中击败了33.03%的用户
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//n&(n-1)
class Solution {
public:
int hammingWeight(uint32_t n) {
int res = 0;
while(n)
{
n &= n-1;
res++;
}
return res;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.8 MB, 在所有 C++ 提交中击败了82.14%的用户
*/

day6

数值的整数次方

简单的快速幂+迭代,假如数值是x,次方的n,那么就是要分n是奇数还是偶数,这是因为如果是奇数要多一项,偶数则直接x平方。把n看成二进制的话,举个例子,如果n=1000,则是$x^8$,把x的二次方再二次方再二次方,整个过程三次即可(因为有三个零),但如果是1001,则要先乘一个x,再乘$x^8$。

也就是说,如果n是奇数,则底数累乘一个“x”。为了循环计算,我们要把n每次除以2(其实就是一位一位看是不是1),然后再看是不是奇数。对应此,所谓的“x”就也是累成的,可以定义一个k存储中间结果。

再注意一下细节,如果n是负数,那么要把x取倒数,然后把n正过来。但是负数的n正过来可能会使int溢出,所以要用longlong来做。

1
实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
示例 1:

输入:x = 2.00000, n = 10
输出:1024.00000
示例 2:

输入:x = 2.10000, n = 3
输出:9.26100
示例 3:

输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25
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
class Solution {
public:
double myPow(double x, int n) {
long long N = n;
if(N<0)//预处理
{
x=1/x;
N=-N;
}

double k = x;
double res = 1;
while(N)
{
if(N%2==1)
res *= k;//如果最后一位是1,说明对应的k要乘
k = k*k;//不管如何,因为N要右移了,k要平方一次
N/=2;//右移
}
return res;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.7 MB, 在所有 C++ 提交中击败了82.47%的用户
*/

day7

打印从1到最大的n位数

这题之所以是简单题,是因为题目设计时不需要考虑大数问题,这类题目最好直接以大数的形式来写,这就需要string来辅助。

由于最后要返回vector,因此定义一个vector成员变量,这里面存int;同时使用两个辅助函数,一个用来递增数,一个用来将string转换为int存到vector里。

public内是主函数,由于定义string需要数的大小n,这是主函数的参数,因此string也要在主函数定义,所以辅助函数也需要传入string这个参数(用引用,递增函数要修改number),具体见代码。然后循环递增,每个数都转int放vector,最后返回就可以了。循环的结束判断利用递增函数的返回值来做,如果溢出则结束(溢出表明数已经大于给定的位数了)

对于转int函数,重点是把string前面多余的’0’去掉,从头开始遍历这些0,当不是0时就退出,然后用另一个string用+=把剩下的都连接起来,最后用stoi函数转int。

对于递增函数,重点是进位。首先定义一个表示进位的变量,然后就能得出每个位置上应该变成的值了:num = number[i]-'0'+takeOver;//当前位等于原来的加上进位的,当然最低位因为递增要加一。takeOver初始化为0,因为最低位没有进位。然后要循环判断进位,因为有可能是…99999的情况。所以我们的循环从最低位开始,最低位的num得出后要num++,然后如果num==10说明要进位,takerOver=1,这一位变成0;同时如果是最高位了,就溢出了,返回false。如果没有进位,就到此为止了,设置这一位的string的值就可以了,最后返回true表示可以继续递增。

1
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
1
2
3
4
示例 1:

输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]
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
class Solution {
public:
vector<int> printNumbers(int n) {
//创建一个能容纳最大值的字符数组,由于需要n,因此在函数里创建而不成为类成员,这导致辅助函数需要传入number这个参数
string number(n,'0');
//初始全部设置为0,因为输出从1开始,后面就先增加1

while(increment(number))//在increment的过程中判断是否结束,因为increment既有到哪一位的信息、也有是否进位的信息
saveNum(number);
return res;
}

private:
vector<int> res;//将string转int,放数组里


bool increment(string &number)//运行一次就+1
{
int len = number.size();
int takeOver = 0;//最大的要点就是考虑进位,一开始的进位是0

for(int i=len-1; i>=0; i--)//i从最后开始,代表数从最低位开始
{
int num = number[i]-'0'+takeOver;//当前位等于原来的加上进位的
if(i==len-1)
num++;//如果是最低位,则要+1,代表增加一个1

if(num==10)//若要进位
{
if(i==0)
return false;//最高位,且加上进位是10,溢出了,结束
else
{
number[i] = '0';
takeOver = 1;//不用再设回0,因为一旦不用进位就结束了
}
}
else//不用进位就到此为止
{
number[i] = num+'0';
break;
}

}
return true;
}

void saveNum(string &number)//这个函数主要是把number前面多余的0去掉
{
string s = "0";
int len = number.size();
int notzero = len;//如果都为0则notzero不会被重新赋值,这会使后面那个循环直接跳过,使得s不变就是"0"

for(int i=0;i<len;i++)
{
if(number[i]=='0')
continue;
else//找到第一个不为0的地方
{
notzero = i;
break;
}
}

for(int i=notzero;i<len;i++)
s += number[i];

int resnum = stoi(s);
res.push_back(resnum);
}
};
/*
执行用时:16 ms, 在所有 C++ 提交中击败了14.22%的用户
内存消耗:11.6 MB, 在所有 C++ 提交中击败了12.52%的用户
*/

day8

删除链表的节点

简单的双指针应用,一个前一个后,cur指针来判定,pre指针要进行节点越过操作

1
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。
1
2
3
4
5
6
7
8
9
10
示例 1:

输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:

输入: head = [4,5,1,9], val = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* deleteNode(ListNode* head, int val) {
if(head->val==val) return head->next;
ListNode *pre = head, *cur = head->next;//现在head不是目标节点,从next开始
while(cur)
{
if(cur->val==val)//如果找到
{
pre->next = cur->next;//越过
break;
}
else//否则两个指针向后
{
pre = cur;
cur = cur->next;
}
}
return head;
}
};
/*
执行用时:8 ms, 在所有 C++ 提交中击败了84.51%的用户
内存消耗:8.9 MB, 在所有 C++ 提交中击败了84.76%的用户
*/

调整数组顺序使奇数位于偶数前面

简单的快排思想的应用,其实就是头尾双指针。

1
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分。
1
2
3
4
5
例:

输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
vector<int> exchange(vector<int>& nums) {
int i=0, j=nums.size()-1;
while(i<j)
{
while(nums[i]%2==1 and i<j)//这个过程要加i<j的判断,一方面防止全是奇数时nums[i]的i越界了,一方面减少循环次数
i++;
while(nums[j]%2==0 and i<j)
j--;
if(i>=j)//减少不必要的交换和动作
break;
swap(nums[i],nums[j]);
//手动推进,可以减少大while或小while的一次判断
i++;
j--;
}
return nums;
}
};
/*
执行用时:8 ms, 在所有 C++ 提交中击败了99.32%的用户
内存消耗:17.5 MB, 在所有 C++ 提交中击败了87.78%的用户
*/

day9

链表中倒数第k个节点

用快慢双指针就很简单了,快指针先走k步(指向第k+1个节点),然后两个指针再一起走直至快指针为null,此时快指针又走了n-k步,慢指针也走了n-k步,倒数过来就是倒数第k个。

1
2
3
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
1
2
3
4
5
示例:

给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.
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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
ListNode *quick = head, *slow = head;
for(int i=0; i<k; i++)
quick = quick->next;
while(quick)
{
slow = slow->next;
quick = quick->next;
}
return slow;
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了68.49%的用户
内存消耗:10.2 MB, 在所有 C++ 提交中击败了73.69%的用户
*/

反转链表

简单的双指针pre和cur,前面的从尾到头打印链表写过了。感觉也可以用辅助栈,不过不推荐。

1
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
1
2
3
4
示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *pre = nullptr, *cur = head;
while(cur)
{
ListNode *tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了95.85%的用户
内存消耗:7.9 MB, 在所有 C++ 提交中击败了93.81%的用户
*/

day10

合并两个排序的链表

简单题,大概是一个merge。使用一个非nullptr的伪头节点能减少代码重复(new一个),当然不用也行,这样还是得要一个head和一个cur,不过先比较l1和l2的头节点大小得出head和cur的指向,然后再进while循环。原因是while内要cur->next,如果cur没有指向节点而是null则它都没有next,只能在while里面再if判断是不是第一次进入,这样每次又多了一个if。

1
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
1
2
3
4
示例1:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
//由于一开始不知道是l1头小还是l2小,因此可以定义一个伪头节点(不是nullptr,所以用new构建一个),这样可以
//使头节点的比较也放在while里,和其他节点一样。这样减少了代码重复
ListNode *cur = new ListNode(0);
ListNode *head = cur;//head用来保存,cur用来移动

while(l1 and l2)//合并
{
if(l1->val < l2->val)
{
cur = cur->next = l1;//添加节点并往下
l1 = l1->next;
}
else
{
cur = cur->next = l2;
l2 = l2->next;
}
}
//合并尾部
cur->next = l1 == nullptr ? l2 : l1;
return head->next;//伪头节点后就是
}
};
/*
执行用时:16 ms, 在所有 C++ 提交中击败了92.19%的用户
内存消耗:18.6 MB, 在所有 C++ 提交中击败了78.85%的用户
*/
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
//不使用伪头节点,先比较获得头节点,代码比较臃肿,但是性能不差
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
//前置处理
if(!l1)
return l2;
if(!l2)
return l1;

ListNode *head;
if(l1->val < l2->val)
{
head = l1;
l1 = l1->next;
}
else
{
head = l2;
l2 = l2->next;
}
ListNode *cur = head;

while(l1 and l2)//合并
{
if(l1->val < l2->val)
{
cur = cur->next = l1;//添加节点并往下
l1 = l1->next;
}
else
{
cur = cur->next = l2;
l2 = l2->next;
}
}
//合并尾部
cur->next = l1 == nullptr ? l2 : l1;
return head;
}
};
/*
执行用时:12 ms, 在所有 C++ 提交中击败了98.83%的用户
内存消耗:18.5 MB, 在所有 C++ 提交中击败了92.94%的用户
*/

顺时针打印矩阵

细节在要注意给的矩阵是不是空的,如果是空要直接返回了,否则会有些越界问题。然后我们先获得上下左右四个边界,然后进入一个大的while循环一遍不断地“绕圈”。然后在while内根据边界右、下、左、上遍历元素,同时更新边界,并判断是否越界,越界就可以退出了。总体下来就是根据“边界”,在while(true)里for循环,知道这个就比较简单了。

1
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
1
2
3
4
5
6
7
8
示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:

输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
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
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> res;
int top = 0, left = 0, bottom = matrix.size()-1, right=0;
if(bottom==-1)//没有元素
return res;
else
right = matrix[0].size()-1;

while(true)
{
for(int i=left;i<=right;i++)
res.push_back(matrix[top][i]);
top++;
if(top>bottom)
break;

for(int i=top;i<=bottom;i++)
res.push_back(matrix[i][right]);
right--;
if(left>right)
break;

for(int i=right;i>=left;i--)
res.push_back(matrix[bottom][i]);
bottom--;
if(top>bottom)
break;

for(int i=bottom;i>=top;i--)
res.push_back(matrix[i][left]);
left++;
if(left>right)
break;
}
return res;
}
};
/*
执行用时:8 ms, 在所有 C++ 提交中击败了84.66%的用户
内存消耗:9.6 MB, 在所有 C++ 提交中击败了73.13%的用户
*/

包含min函数的栈

使用双栈来简化操作,一个主栈就进行push、pop和top(不进行其他操作),另一个辅助栈维护min,这样设计就能明确要做什么。

为了维护min,辅助栈的每次push就需要比较,除了空的时候直接放入,后面的push都只放入不大于栈顶的值。因为大于栈顶的值必然不可能再成为最小值了,它会在最小值被pop之前pop(因为先后顺序的原因),同时相等的元素要放入,因为pop了一个最小值,剩下的也可以是最小值。对于pop,只有当主栈pop出去的是最小值时,辅助栈才pop,因此要判断相不相等。

这样,返回min就只用返回辅助栈的top。实际上,核心是将辅助栈设计成一个升序栈(从顶到底),原理是因为后来的更大的值不可能成为最小值。

1
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
1
2
3
4
5
6
7
8
9
10
示例:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.min(); --> 返回 -2.
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
class MinStack {
public:
/** initialize your data structure here. */
stack<int> xstack;
stack<int> hstack;//help stack,辅助栈维护升序栈
MinStack() {

}

void push(int x) {
xstack.push(x);
if(hstack.empty())
hstack.push(x);
else
if(x<=hstack.top())
hstack.push(x);
//辅助栈维护最小值,因此只有更小的才放进去。大的不放是因为辅助栈的顶部一定是最小值,假如说这个最小值被pop出去不存在了
//那么这个更大的值肯定也更早被pop出去(因为最小值更先存在,大的在更顶上),所以这个最大值不会成为最小值,没必要放进去。
//使用等于判断是因为可能有多个最小值,pop出一个还有其他的也算

}

void pop() {
int x = xstack.top();//要看辅助栈的最小值要不要pop出去
xstack.pop();
if(x==hstack.top())
hstack.pop();//如果主栈pop出去的是一个最小值,那么这个最小值也要pop
}

int top() {
return xstack.top();
}

int min() {
return hstack.top();
}
};
/*
执行用时:12 ms, 在所有 C++ 提交中击败了98.07%的用户
内存消耗:14.6 MB, 在所有 C++ 提交中击败了86.44%的用户
*/

栈的压入、弹出序列

这题主要用模拟,根据生活中“手动判断”的过程来模拟。这是怎么样的过程呢:我们一般会跟踪元素一个个push的过程,然后对比poped序列,一旦一个元素可以pop,那就pop并且把前面能pop的也pop。这是因为数字都是不同的,如果错过了pop时机,再有元素进来就不再能pop了,也就错了。

而这里给的两个序列都是vector,我们模拟要不断pop,这不太方便,所以用到一个辅助栈(这也就是我们手动模拟用到的容器)。这样模拟就是:把pushed一个一个放进辅助栈(pushed如同数组,所以用for循环放直观一些),每放进一个就查看poped序列(记录好上次查看的位置),如果相等就pop,然后poped序列向后继续比较看能不能pop(这就用while循环,因为while直接能进行比较判断,并且也不知道for的次数)。

辅助栈不断加入元素,并且在合适时pop,如果最后辅助栈是空的,那么就是正确的了。

1
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
1
2
3
4
5
6
7
8
9
10
11
12
示例 1:

输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:

输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。
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
class Solution {
public:
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
if(pushed.size()!=popped.size())
return false;//先考虑大小,不同直接false
int n = pushed.size();
if(n==0)
return true;//如果大小为0就true

//然后开始模拟,pushed和popped是不同排列,所以数字相同
//如果直接对pushed栈模拟,不好操作,因为pushed是个vector,不对顶操作
//所以用一个辅助栈
stack<int> st;
int pop_j = 0;//指向popped的位置

for(int i=0;i<n;i++)//注意pushed和popped是vector而不是stack,要以数组形式使用
{
st.push(pushed[i]);//不断按顺序放入元素
while(!st.empty() and st.top()==popped[pop_j])//然后尝试倒出,如果能倒则一直倒出,
//因为数字不同正确性是唯一的,能倒时不倒,下一个进来时就不可能再倒出了
//!st.empty()不能漏,因为top()在没有元素时出错、popped[pop_j]可能会越界,也不能直接判断pop_j,
//因为存在st空了但pop_j还没越界的情况,使用st一举两得
{
st.pop();
pop_j++;
}
}
return st.empty();
}
};
/*
执行用时:8 ms, 在所有 C++ 提交中击败了70.50%的用户
内存消耗:14.8 MB, 在所有 C++ 提交中击败了73.76%的用户
*/

day11

复杂链表的复制

这题有难度,主要在于没见过不太好想。先从正常的复制开始,如果只是复制next,那么我们遍历一遍原来的链表就可以得到next的信息了。为什么不能得到random呢,原因是random指向的那个节点不知道在哪里,不可能再用一层遍历去找。你可能会想着先遍历复制next的信息,再遍历一遍得到random,这里的关键问题是,我们在第二遍遍历的时候,确实是可以知道原来链表的节点random指向的位置,假设为A,但新的链表的节点random指针要指向的节点在哪呢?假设这个节点叫B,我们的问题是不能从A来找到B,B还是未知的。

因此,重点就是解决这个问题。简单的方法是,就把每个新的节点先放在原来节点的后面,这样就可以用next来找到复制的节点。因此上面的B就是A->next。于是就建立好了关系:cur->next->random = cur->random->next;。然而random可以指向null,没有next,所以要判空。

最后执行两个链表的拆分即可。整个过程就是:原地拷贝延申、修改random、拆分。注意拷贝要用new。

1
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
1
2
3
4
5
6
7
8
9
10
11
12
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。
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
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;

Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
//前置判空
if(!head)
return nullptr;

//原地拷贝延申
Node *cur = head;
while(cur)
{
Node *newcur = new Node(cur->val);
newcur->next = cur->next;
cur->next = newcur;
cur = newcur->next;
}
cur = head;

//修改random指针
while(cur)
{
if(cur->random)//如果这个random不是null才有意义
cur->next->random = cur->random->next;//cur->next表示那个新复制的节点,然后->random表示修改指向,
//指向的是cur->random这个节点的next,也就是对应的新复制的节点
cur = cur->next->next;
}

//拆分
cur = head;
Node *newhead = head->next;//记录下来,因为要用到next,所以head不能为null,因此前面要判断是否为null
while(cur)
{
Node *newcur = cur->next;
cur->next = newcur->next;//cur非null,那么newcur非null,但newcur->next可能是null,也即这是最后一对节点
if(newcur->next)//如果不是null
newcur->next =cur->next->next;
else//否则直接是null,因为没有null->next
newcur->next = nullptr;
cur = cur->next;
}
return newhead;
}
};
/*
执行用时:8 ms, 在所有 C++ 提交中击败了73.50%的用户
内存消耗:10.9 MB, 在所有 C++ 提交中击败了90.72%的用户
*/

day12

字符串的排列

挺难的一道题,我用的是“下一个排列”的方法。

下一个排列的方法很难理解,一共有四步:1.从后往前找到第一个(严格)升序的元素对,这个元素对的前一个是“较小数”,后面那一段都是降序(非严格,跳过相同的字符,这里面i和j的比较都加”=”)的;2.从后往前找到第一个比“较小数”大的数,这个数是“较大数”;3.“较小数”和“较大数”交换;4.交换后,降序的那段依然降序,要反过来变成升序(用reverse函数或前后双指针swap)。在第一步中,我们用i和i+1判断元素对,如果字符串已经是最后一个排列了,或字符串是全相等的(或部分相等,不需要再排列了)时,i会变成-1(找不到),则此时要返回false了。

有了下一个排列,就可以慢慢获得所有排列了,首先就是要把原字符串sort变成最小的排列,然后do-while(因为第一个排列总是要放进去的),注意do-while的while有”;”。

1
2
3
输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
1
2
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
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
class Solution {
public:
vector<string> permutation(string s) {
vector<string> res;
sort(s.begin(), s.end());//因为是取下一个排列遍历,所以先排序得到最小排列
do
{
res.push_back(s);
}while(nextpermutation(s));
return res;
}
bool nextpermutation(string &s)
{
int i = s.size()-2;
//加等号是因为字符串可能有相同字符,这里要加等号越过它们,表示重复的只有一种情况;
//否则i会停在重复的字符,j会更向前,导致前面的情况又换回来,进入死循环
while(i>=0&&s[i]>=s[i+1])//从右向左找到第一个非降序的,即突然凹下去的那里
i--;
if(i<0)
return false;//如果都是降序的,说明已经是最后一个排列了
int j = s.size()-1;
while(s[i]>=s[j])
j--;//从右向左找到第一个比a[i]大的
//if(j<0)
//return false;//如果字符串都是相等的就可能一直往前走越界,但这种情况已经被i判断了,不用在j这考虑
swap(s[i],s[j]);
//现在后面i+1开始那一段是降序的,反转一下变成升序会更小
for(int n=i+1,m=s.size()-1;n<m;n++,m--)
swap(s[n],s[m]);
//可以调库reverse(s.begin() + i + 1, s.end());
return true;
}

};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:16.9 MB, 在所有 C++ 提交中击败了99.15%的用户
*/

数组中出现次数超过一半的数字

摩尔投票法,记住两个变量:候选者、投票数。如果没有计数就重置候选者,然后通过比较候选者和当前值看票数要加一还是减一。知道这个方法就很简单了。

1
2
3
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的组总是存在多数元素。
1
2
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
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
//正常逻辑版本
class Solution {
public:
int majorityElement(vector<int>& nums) {
int count = 0;
int cand;
for(int num:nums) //这是python取数组内容的形式,c++11也支持(加个变量类型即可),也可以for(int i=0;i<nums.size();i++)用nums[i]
{
if(!count)//如果没有计数,则重新开始投票
{
cand = num;
count++;
}
else if(num==cand)//如果有计数说明有候选者,相等则计数++
count++;
else//即有计数,也不相等
count--;
}
return cand;//题目说一定有众数,就直接返回;否则要再检验一遍,因为此时不一定是众数
}
};
/*
执行用时:16 ms, 在所有 C++ 提交中击败了65.56%的用户
内存消耗:18.2 MB, 在所有 C++ 提交中击败了66.76%的用户
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//优化版本
class Solution {
public:
int majorityElement(vector<int>& nums) {
int count = 0;
int cand;
for(int num:nums) //这是python取数组内容的形式,c++11也支持(加个变量类型即可),也可以for(int i=0;i<nums.size();i++)用nums[i]
{
if(!count)//如果没有计数,则重新开始投票
cand = num;
count+= num==cand?1:-1;//无论如果都判断一次,这里把count是不是0的情况都包含了
//因为count是0也是count++
//这种方式也就是把上面的else if的else去掉了,变成两个独立的if而不是一个大if,会快一些。

}
return cand;//题目说一定有众数,就直接返回;否则要再检验一遍,因为此时不一定是众数
}
};
/*
执行用时:8 ms, 在所有 C++ 提交中击败了97.96%的用户
内存消耗:18.2 MB, 在所有 C++ 提交中击败了85.24%的用户
*/

最小的k个数

方法是快速排序,使用快排的思想,注释写了很多了,能达到O(n)的时间复杂度

1
输入整数数组 `arr` ,找出其中最小的 `k` 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
1
2
3
4
5
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

输入:arr = [0,1,2,1], k = 1
输出:[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
class Solution {
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
int size = arr.size();
if(size == k)//刚好就不用排了
return arr;
quickSelect(arr,k,0,size-1);//快速选择:把最小的k个放在最前面
vector<int> res;
for(int i=0;i<k;i++)
res.push_back(arr[i]);//前k个都是小的了,拷贝一下
return res;
}
private:
void quickSelect(vector<int>& arr, int k, int l, int r) {
int i=l,j=r;
while(i<j)//先快排
{
while(i<j&&arr[j]>=arr[l])
j--;
while(i<j&&arr[i]<=arr[l])//加等号使得相同元素相对位置不改变,稳定排序
i++;
swap(arr[i],arr[j]);

}
swap(arr[i],arr[l]);
//快排要把arr[l]放在两段的中间,就需要保证arr[i]是一个不比arr[l]大的数;
//因此那两个while必须先从j开始,因为当j停下时,要么是碰到了i(上一轮的i已经是小的了),这时全部结束,i是较小的
//要么是等待置换,此时轮到i走,要结束只能碰到j,j在等待,是较小的
//如果while先对i做,可以想到i可能停在上一轮的j处,此时j是大的
//因此如果用左边界,则要从右开始,置换i;用右边界则从左开始,置换j


//此时快排做完,i和i前的元素都是小的,前i+1个元素都是小的,且前i个元素小于第i+1个元素(i代表第i+1个元素)
//与快排不相同的是,这里分情况再排,而不是两段直接排
if(i==k||i==k-1) return;//i=k或者i+1=k
else if(i>k) quickSelect(arr,k,l,i-1);//i左边元素有些多,对左边再排,i本身不用排了,不可能是
else quickSelect(arr,k,i+1,r);//i<k-1,这里只用排k-i-1个元素了,但为什么参数仍然是k呢?
//因为我们是从左边界i+1开始的
//排序只对l-r之间的元素,但位置i仍是整体的,并不是从左边界开始从0算起,因此参数仍是k
}
};
/*
执行用时:16 ms, 在所有 C++ 提交中击败了97.66%的用户
内存消耗:18.5 MB, 在所有 C++ 提交中击败了52.99%的用户
*/

day13

连续子数组的最大和

动态规划,递推方程是:dp[i] = max(dp[i-1],0)+nums[i];。dp[i]的意思是前i个数的子数组的最大和,则dp[i]是前面的最大值加上nums[i],其中如果前面的最大值是一个负数就从头开始,就是0+nums[i]。加上nums[i]才使得这样的递推的子数组是连续的,因为dp[i-1]也加上了nums[i-1],如果大于0,那么使用它的话就是连续的子数组了。

这里面动态规划注意一个要点,如果递推式不利用历史信息的话,只利用前面一项或几项,那就可以用一个或几个变量代替dp数组,能把空间复杂度从O(n)变成O(1)。

1
2
3
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。
1
2
3
4
5
示例1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//动态规划普通版本,使用dp数组递推
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
int dp[n];
dp[0] = nums[0];
int res = dp[0];
for(int i=1;i<n;i++)
{
dp[i] = max(dp[i-1],0)+nums[i];
res = max(res,dp[i]);
}
return res;
}
};
/*
执行用时:16 ms, 在所有 C++ 提交中击败了81.76%的用户
内存消耗:22.8 MB, 在所有 C++ 提交中击败了27.27%的用户
*/

这里面呢,实际上dp数组在遍历时只用到前面一项,更之前的信息完全不用,实际上就可以简化为两个变量cur和pre,一个代表dp[i],一个代表dp[i-1]。更进一步,cur和pre只是前一轮和这一轮的关系,用一个变量完全足够了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
int pre = nums[0];
int res = pre;
for(int i=1;i<n;i++)
{
/*
等价于:
cur = max(pre,0)+nums[i];
pre = cur;
*/
pre = max(pre,0)+nums[i];//其实就是pre = cur = max(pre,0)+nums[i]; cur完全不需要。
res = max(res,pre);
}
return res;
}
};
/*
执行用时:12 ms, 在所有 C++ 提交中击败了95.96%的用户
内存消耗:22.3 MB, 在所有 C++ 提交中击败了88.01%的用户
*/

数据流的中位数

这题是困难的,比较难想,不会就快排再算。这里考虑的是数据流,比较偏应用,就是要用一个合适的数据结构来做(也说了要选数据结构)。这里插入对于数据结构都好说,主要是中位数怎么找。根据中位数的性质可以把数据均分成较大的一组和较小的一组,然后只要找到两个数据组中“突出”的那一个就好了,也就是中位数或两个用于计算中位数的数。我们只要一个或两个数,这些数是较大组的最小值或较小组的最大值(这样才居中)。

因此就可以用堆,因为堆就是用来存最大值和最小值的,在c++中可以用优先级队列priority_queue来做。堆有两个,一个小顶堆存较大数据的最小值、一个大顶堆存较小数据的最大值。这里的重点是维护数据均分,如果是偶数则两个堆数据个数相同,如果是奇数则其中任意一个多一个数据,这里选大顶堆多一个元素。

因此就分两种情况插入数据,如果插入前两个堆大小相等,则要向大顶堆插入。但不知道这个数据多大,因此不能直接插入,而需要先插入小顶堆,再从小顶堆拿出(pop)顶部元素插入大顶堆。这个过程就是把新元素和较大元素比较,拿出最小的放入大顶堆,保证了大顶堆中的元素全都小于小顶堆。如果插入前大顶堆个数多(根据设计不可能小顶堆更多),则要向小顶堆插入,同样的要先插入大顶堆,然后拿出顶部元素插入小顶堆。

最后是取中位数,如果堆大小相等,则取两个堆顶部元素(即数据流大小最中间的两个元素)取平均;否则大顶堆多一个元素,根据大小关系直接返回大顶堆的顶部元素。注意这里取平均在除以2之前要*1.0转double。

这样插入时间复杂度是O(logn),取中位数是O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
1
2
3
4
5
6
7
8
9
10
11
12
示例 1:

输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
示例 2:

输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]
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
class MedianFinder {
public:
/** initialize your data structure here. */

// 大顶堆,存储较小的一半的数据,堆顶为最大值
priority_queue<int, vector<int>, less<int>> maxHeap;//less表示降序排序
// 小顶堆, 存储较大的一半的数据,堆顶为最小值
priority_queue<int, vector<int>, greater<int>> minHeap;//greater表示升序排序
//第一个参数是类型、第二个参数是底部容器(使用heap的算法)、第三个参数是比较方式

MedianFinder() {
}

// 维持堆数据平衡,并保证左边堆的最大值小于或等于右边堆的最小值
void addNum(int num) {
/*
* 当两堆的数据个数相等时候,向大顶堆添加元素(也可以向小顶堆添加,指定一个堆放多的元素)。
* 采用的方法不是直接将数据插入大顶堆,而是将数据先插入小顶堆(因为这个元素大小不好说),算法调整后
* 将堆顶的数据(较大数中最小的)插入到大顶堆,这样保证大顶堆插入的元素始终比小顶堆的元素小。
* 同理如果大顶堆数据多,往小顶堆添加数据的时候,先将数据放入大顶堆,选出最大值(top)放到小顶堆中。
*/
//这种添加方式是让奇数个数时多的那一个放到大顶堆中,实际上也可以放小顶堆中,方式是镜像的
//但使用一种方式后,其他的操作要适应它。因为这些数据个数决定了中位数的取法。

if (maxHeap.size() == minHeap.size()) {
minHeap.push(num);//先放小顶堆
int top = minHeap.top();//把较大的值中最小的那个拿出来
minHeap.pop();//pop
maxHeap.push(top);//给大顶堆
} else {
maxHeap.push(num);
int top = maxHeap.top();
maxHeap.pop();
minHeap.push(top);
}
}

double findMedian() {
if (maxHeap.size() == minHeap.size())
return (maxHeap.top()+minHeap.top())*1.0/2;//转double
else
return maxHeap.top()*1.0;

}
};
/*
执行用时:92 ms, 在所有 C++ 提交中击败了75.35%的用户
内存消耗:40.6 MB, 在所有 C++ 提交中击败了84.11%的用户
*/

day14

数字序列中某一位的数字

本质是找规律的题目,个位数有9个(下标从0开始,序列算了0,相当于抵消)、个位数有90个、百位数有900个…根据这些规律就能得出n在那一个位数的阶段中,比如n=11就在10~99这个两位数的段里。然后就能得出从这个段起始开始还要走多少个数字x(因为前面的总数字数肯定是知道的,这样才能得到在哪个段嘛),根据这个x和位数的关系又能得出是第几个数的第几位,然后转string取出来就好了。

注意这里当n比这一位数和前面位数的数字要多时,要算后面的位数的总个数,但是这样可能会越界,因为乘以了9,数没有那么多。那么就要有溢出判断,不应该乘以9时就不做了。

这种题还是用些实例来想过程,比如取11、12等等。

1
2
3
数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。

请写一个函数,求任意第n位对应的数字。
1
2
3
4
5
6
7
8
示例 1:

输入:n = 3
输出:3
示例 2:

输入:n = 11
输出: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
class Solution {
public:
int findNthDigit(int n) {
if(n<10) return n;
int intmax = 0x7fffffff;
bool flag = true;//溢出判断,false是溢出
int digit = 1;//位数
int total = 9;//总数
int start = 1;//起始点,因为每个起始都是10、100、1000这样的,每次乘10就可以
//因为下标也从0开始计数,所以n=9时对应的是9,因此total为9表示个位数
while(n>total)//首先确定n代表的数在哪个位数中
{
digit++;//表示进入下一位数的范围
start *= 10;
if(digit*start>=intmax/9)//做乘法会溢出,不做了
{
flag = false;
break;
}
total += digit*start*9;//下一位数有9*start个数,每个数有digit位,这就能算出下一位数的数字个数
}

//
int x;//x是从start开始的数字个数
if(flag)//如果没溢出
x = n-total+digit*start*9;
else //如果要溢出,说明最后的total是没做的
x = n-total;//前面有total个单个的数字

int num = start + (x-1)/digit;//第num个数,x-1是因为start本身就是第一位,比如10的话应该是10+0/digit,但此时x是1
int k = (x-1)%digit;//k是指第num个数的第k位,x要减一同理

string nums = to_string(num);//to_string函数把int转string
return nums[k]-'0';//这里nums[k]是char要转一下
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.7 MB, 在所有 C++ 提交中击败了89.87%的用户
*/

把数组排成最小的数

这题也比较难想,一般直接去想会想成遍历或规划问题。实际上这题被做成了一道排序问题,假如有数字x,y,如果x+y>y+x,那么x应该放在y前面(后面充要证明)。当x和y相邻时这很好理解,但当xy不相邻时,为什么x一定要在y前面呢?必要性:我们假设有一个最小的排列axyzb,我们很容易得到a<x,x<y,y<z,z<b,否则我们可以交换相邻的元素得到更小的排列,即假如z>b我们可以交换z和b。充分性:如果a<x,x<y,y<z,z<b,那么按axyzb的排列一定最小,因为交换 a,b(表示任意交换两个元素)相当于依次交换 ax,ay,az,接着交换 ab,zb,yb,xb 。每一次相邻交换都使得交换后的值更大。

因此,如果x+y>y+x,那么x应该放在y前面。那么我们对这个数组进行排序就可以了,能够排序的原因是每两个元素都能得到明确的大小关系和可传递性(一旦x>y,y>z,那么x>z,即如果x在y前面,y在z前面,根据前面的充要证明,x一定在z前面),这和正常的比大小一样。因此能使用快排来做,每次都和左边界标值比较,看x应该放在标值前还是后。

将数组转string的vector,然后排序,再拼接就可以了。

1
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
1
2
3
4
5
6
7
8
示例 1:

输入: [10,2]
输出: "102"
示例 2:

输入: [3,30,34,5,9]
输出: "3033459"
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

class Solution {
public:
string minNumber(vector<int>& nums) {
//先转string
vector<string> str;
int n = nums.size();
for(int i=0;i<n;i++)
str.push_back(to_string(nums[i]));

quickSort(str,0,n-1);

string s = "";
for(string strs : str)
s += strs;
return s;
}

private:
void quickSort(vector<string>& str,int l,int r)//排序
{
if(l>=r) return;
int i=l, j = r;//左边界右边界
while(i<j)
{
while(i<j and str[l]+str[j]<=str[j]+str[l]) j--;
//j--的情况是前+后的值小于后+前,前是标值l,后是值j,此时说明不用换
while(i<j and str[l]+str[i]>=str[i]+str[l]) i++;
swap(str[i],str[j]);
}
swap(str[i],str[l]);

quickSort(str,l,i-1);
quickSort(str,i+1,r);
}
};

/*
执行用时:4 ms, 在所有 C++ 提交中击败了91.34%的用户
内存消耗:11 MB, 在所有 C++ 提交中击败了53.92%的用户
*/

day15

把数字翻译成字符串

通过读题目可以发现,要么对一位数字直接翻译,要么对两位数字进行翻译,即1或2,这和动态规划的青蛙跳台阶很像。对于第i位结尾的数字,如果能知道第i-1位结尾时翻译总数和第i-2位结尾时的翻译总数,那么就有f(i)=f(i-1)+f(i-2)。这个递推式是解释是,第i位要么单独翻译要么和i-1位一起翻译,能这么做的原因是这两种翻译出来的字符串肯定不一样。

然而还需要考虑的是,i和i-1位组成的数字不一定能够翻译(10–25之间),因此在不能翻译时,递推式退化成f(i)=f(i-1),这只是一个if的事情。

根据这个递推式,就可以使用动态规划,由于最多往前两项,那么用三个变量即可,不用用数组。

1
2
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。
一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
1
2
3
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
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
class Solution {
public:
int translateNum(int num) {
//翻译范围是0-9,10-25
//f(i) = f(i-1)+f(i-2),其中i和i-1要能满足翻译范围,否则f(i) = f(i-1)
int pre = 0;//f(i-1)
int prepre = 0;//f(i-2)
int cur = 1;
string str = to_string(num);

for(int i = 0;i<str.size();i++)
{
prepre = pre;
pre = cur;
if(i==0)
continue;
string s = str.substr(i-1,2);//拿出i和i-1
/*
* string substr (size_t pos = 0, size_t len = npos) const;
* 在字符位置pos开始,跨越len个字符(或直到字符串的结尾,以先到者为准)对象的部分。
*/
if(s<="25"&&s>="10")
cur = pre + prepre;
else
cur = pre;


}
return cur;

}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.7 MB, 在所有 C++ 提交中击败了82.92%的用户
*/

礼物的最大价值

一眼动态规划。dp[i][j] = max(dp[i-1][j],dp[i][j-1])+gird[i][j],从左边或上边到来,选一个大的路径加上本身(价值大于0)。这里巧妙的地方是,不需要一个额外的dp[n][m]数组,可以直接修改grid,因为遍历到grid某处时的值只需要使用自身和前面的累加值,那么在使用了自身后就可以把自己修改成累加值,以后不用自身原来的值了:grid[i][j] = max(grid[i-1][j],grid[i][j-1])+gird[i][j],简便写法:grid[i][j] += max(grid[i-1][j],grid[i][j-1])。最后,最大值一定是右下角元素,因为所有礼物最大值都大于0,路径一定会走到那里。

1
2
3
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。
你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。
给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
1
2
3
4
5
6
7
8
输入: 
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
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
class Solution {
public:
int maxValue(vector<vector<int>>& grid) {
int n = grid.size(), m = grid[0].size();
//dp[i][j] = max(dp[i-1][j],dp[i][j-1])+gird[i][j],从左边或上边到来,选一个大的路径加上本身(价值大于0)
//vector<vector<int>> dp;不用额外的数组,在grid原地修改即可,
//因为grid本身的数组并不需要,后面的计算使用的是前面的累加

//初始化dp数组初始化
for(int i = 1;i<m;i++)//累加第一行
grid[0][i] += grid[0][i-1];//第一行右边的价值只能由左边累加
for(int i = 1;i<n;i++)//累加第一列
grid[i][0] += grid[i-1][0];//第一列下边的价值只能由上边累加

//开始动态规划
for(int i=1;i<n;i++)
for(int j=1;j<m;j++)
grid[i][j] += max(grid[i-1][j],grid[i][j-1]);

//最后一定走到右下角,因为价值大于0
return grid[n-1][m-1];
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了95.90%的用户
内存消耗:8.7 MB, 在所有 C++ 提交中击败了94.41%的用户
*/

day16

最长不含重复字符的子字符串

这种字符串的问题,可以考虑以第i位为结尾的后缀怎么怎么样。设dp[i]是以第i位结尾的字符串的最长不含重复字符的子字符串的长度,注意这里隐含地说明了第i位一定在这个字符串里,这给连续性带来了方便。

那么假如知道了dp[i-1],那么dp[i]怎么得出呢?我们只需要知道s[i]上一次出现的位置即可(假设是j)。如果上一次出现的位置在dp[i-1]到i的范围内,那么这个子串需要缩小,dp[i]=i-j,从j开始算。否则,i在dp[i-1]前面或者根本没出现,这两种情况都可以直接dp[i]=d[i-1]+1,即s[i]和前面连接

如何记录上一次s[i]的位置呢?用一个哈希表存储,map[s[i]]表示和s[i]相同的字符上一次出现的位置

1
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
示例 1:

输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
  请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
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
class Solution {
public:
int lengthOfLongestSubstring(string s) {
/*
这种字符串的问题,可以考虑以第i位为结尾的后缀怎么怎么样
设dp[i]是以第i位结尾的字符串的最长不含重复字符的子字符串的长度,
注意这里隐含地说明了第i位一定在这个字符串里,这给连续性带来了方便

那么假如知道了dp[i-1],那么dp[i]怎么得出呢?我们只需要知道s[i]上一次出现的位置即可(假设是j)。
如果上一次出现的位置在dp[i-1]到i的范围内,那么这个子串需要缩小,dp[i]=i-j,从j开始算
否则,i在dp[i-1]前面或者根本没出现,这两种情况都可以直接dp[i]=d[i-1]+1,即s[i]和前面连接

如何记录上一次s[i]的位置呢?用一个哈希表存储,map[s[i]]表示和s[i]相同的字符上一次出现的位置
*/
int n = s.size();
if(n<=1) return n;//因为要创建dp和map,如果n是0就出问题。这里顺便把n=1的情况也一起干了

vector<int> dp(n,0);
unordered_map<char,int> map;

dp[0] = 1;
int max_v = 1;
map[s[0]] = 0;

for(int i=1;i<n;i++)
{
//map.find()在找到元素时返回迭代器,否则返回map.end()。当还不确定找不到得到时,先判断一下
if(map.find(s[i]) == map.end())//没有这个元素,如果直接map[s[i]]就报错了,所以这里的顺序很重要
dp[i] = dp[i-1]+1;
else if(map[s[i]]<i-dp[i-1])
dp[i] = dp[i-1]+1;
else
dp[i] = i-map[s[i]];

map[s[i]] = i;//更新
max_v = max(dp[i],max_v);//每次都要和dp[i]比较,这是这个解法的核心:每个不同位置结尾的子串都可能最长
}
return max_v;

}
};

丑数

简单的规律是一个丑数乘以2、3或5能得到更大的丑数,更进一步可以推导到:每个丑数都无非是前面的丑数乘2、3或5不断增加得来的。那么我们可以用动态规划,把前面得到的丑数不断乘以2、3、5就能得到更大的、后面的丑数

这里的问题是如果单单对一个数同时乘以2、3、5,那么会导致顺序不对,我们明确了要第n个丑数,因此这里有个排序的问题。除了排序的问题,还有重复的问题(如2*5和5*2是同一个丑数)

因此好的解法是,当要求下一个丑数时,一定是某些数(不一定是同一个数)乘2、3或5,把最小的那个拿来,然后把对应的数移向下一个(这导致了数不同)并判断重复,如果重复了这个数也要后移。因为上一个丑数是更前面的某些数乘2、3或5得到的最小值,且考虑了重复问题,那么这个新的最小的丑数比上一个丑数要大,且会比下一个丑数要小,这就有顺序了。

1
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
1
2
3
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
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
class Solution {
public:
int nthUglyNumber(int n) {
//每个丑数都无非是前面的丑数乘2、3或5不断增加得来的
//那么我们可以用动态规划,把前面得到的数字不断乘以2、3、5就能得到更大的、后面的丑数
//这里的问题是如果单单对一个数同时乘以2、3、5,那么会导致顺序不对,因此这里有个排序的问题。
//除了排序的问题,还有重复的问题
//因此好的解法是,当要求下一个丑数时,一定是某些数乘2、3或5,把最小的那个拿来
//然后把对应的数移向下一个并判断重复,如果有重复其他也要后移
int a = 0, b = 0, c = 0;//分别指向下一次要乘2、3、5的位置
vector<int> dp(n);//记录第i个丑数
dp[0] = 1;//初始化
for(int i=1;i<n;i++)//重复n次
{
int numa = dp[a]*2, numb = dp[b]*3, numc = dp[c]*5;
dp[i] = min(numa,min(numb,numc));

//如果dp[i]等于这些数的某个或某几个,说明是使用了或者有重复,要向下跳一个
if(numa == dp[i])
a++;
if(numb == dp[i])
b++;
if(numc == dp[i])
c++;
}
return dp[n-1];//返回第n个丑数
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:7.4 MB, 在所有 C++ 提交中击败了73.97%的用户
*/

day17

第一个只出现一次的字符

哈希表查看有没有重复,然后再遍历一次找到第一个没重复的即可

1
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
1
2
3
4
5
6
7
8
示例 1:

输入:s = "abaccdeff"
输出:'b'
示例 2:

输入:s = ""
输出:' '
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
class Solution {
public:
char firstUniqChar(string s) {
int n = s.size();
if(n==0) return ' ';

unordered_map<char,bool> map;

//遍历查看重复
for(int i=0;i<n;i++)
{
if(map.find(s[i])==map.end())//用find函数看是否已存在
map[s[i]] = true;
else
map[s[i]] = false;
}

//遍历查询第一个
for(int i=0;i<n;i++)
if(map[s[i]])//按s[i]的顺序来遍历
return s[i];
return ' ';
}
};
/*
执行用时:40 ms, 在所有 C++ 提交中击败了39.13%的用户
内存消耗:10.4 MB, 在所有 C++ 提交中击败了75.44%的用户
*/

数组中的逆序对

算法课写过的一道题,可以使用归并排序,只需要添加一点细节就可以了。这个算法的核心是,在归并排序merge的过程中,我们有两个指针指向前一段和后一段,如果后一段的元素要放上去,说明这个元素比前面一段的剩余元素要小,这就产生了逆序对,数量是前面那一段剩余的元素。而当我们把递归的小的段排好后,把这一段产生的逆序对给上层累加,上层继续merge就又可以计算了。排序并不会影响逆序对的数量,因为前一段和后一段分别有序,也不影响前后之间的相对关系。

1
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
1
2
输入: [7,5,6,4]
输出: 5
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
class Solution {
public:
int reversePairs(vector<int>& nums) {
int n = nums.size();
vector<int> tmp(n);
return mergeSortCount(0,n-1,nums,tmp);
}
private:
int mergeSortCount(int left, int right, vector<int>& nums, vector<int>& tmp)
{
if(left>=right) return 0;
int middle = left+(right-left)/2;

//最终结果是这一次归并加上左右递归归并产生的逆序对,因为递归时已经排序好了,那个过程中记录的逆序对数要返回上层
int res = mergeSortCount(left,middle,nums,tmp)+mergeSortCount(middle+1,right,nums,tmp);

//现在左右两段分别是有序的,要创建一个辅助空间,有三种方式
/*
* 1.vector<int> tmp = nums;//辅助空间,如果这样写每次都要拷贝一个很大的nums,会超时
* 2.
* int tmp[right-left+1];
* for(int k=0;k<right-left+1;k++)
* tmp[k] = nums[left+k];//创建对应位置的tmp,不过后面使用tmp就要注意下标了
* 3.像1一样创建一个大的全局tmp,这使得下标能和nums对应,同时使用2的做法,只
* 在使用时拷贝对应的元素,这就使得全过程只进行了线性拷贝
* 这个使用时是指在底层的两段排序好后,像2一样,不过既然是全局的,就要用引用传入参数
*/
//tmp拷贝元素
for(int k=left;k<=right;k++)
tmp[k] = nums[k];

int i = left, j = middle+1;//双指针分别指向两段的开头
int cur = left;//指向nums数组
while(i<=middle and j<=right)
{
if(tmp[i]<=tmp[j])
nums[cur++] = tmp[i++];//赋值同时指针移动

else//后面的小于前面的,只有这时要产生逆序对,所有i-middle的元素都可以和j构成逆序对
{
nums[cur++] = tmp[j++];//赋值同时指针移动
res += middle-i+1;//i-middle共有middle-i+1个元素
}
}
//收尾
while(i<=middle) nums[cur++] = tmp[i++];//前一段剩下的都大于后一段,不过对应的j要产生的逆序对在前面的while产生完了
while(j<=right) nums[cur++] = tmp[j++];//后面的大于前面的
return res;
}
};
/*
执行用时:144 ms, 在所有 C++ 提交中击败了86.13%的用户
内存消耗:43.3 MB, 在所有 C++ 提交中击败了64.45%的用户
*/

day18

两个链表的第一个公共节点

这个算法的核心是,A指针走完A就从B的头开始走,B指针走完B就从A的头开始走,那么它们就能在走过相同步长后在相交点相遇。

假如没有相交点,最终会同时到达nullptr(等长则第一轮抵达,不等长则第二轮抵达)这就使得我们可以和判断相交一样,采用判断A==B的形式,此时退出循环刚好返回nullptr,这在注释里有更详细的解释。

1
输入两个链表,找出它们的第一个公共节点。
1
2
3
4
5
6
7
8
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
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
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if(headA==nullptr || headB == nullptr) return nullptr;//有没有都不影响算法的正确性
ListNode *A = headA, *B = headB;

/*
判断是否要跳转或是否往下走,而不是先往下走再判断或者先判断再往下走
如果先往下再判断,那么while中A和B就不可能是nullptr
如果先判断再往下走,那么跳到head之后总会next,而head可能是相交节点
因此跳转和next是互斥的,不能同时做(要同时的话需要其他辅助手段)

当没有交点时,最终会同时到达nullptr(等长则第一轮抵达,不等长则第二轮抵达)
采用判断的话,A和B可以在执行next后同时到达nullptr达成break条件

而有交点时,如果A和B等长,则在第一轮中间就结束返回
如果不等长,则第一轮A和B不会同时为nullptr,是nullptr就跳,不是就往下
*/

while(A != B)
{
if(A == nullptr) A = headB;//跳转
else A = A->next;

if(B == nullptr) B = headA;
else B = B->next;
}

return A;
}
};
/*
执行用时:32 ms, 在所有 C++ 提交中击败了97.75%的用户
内存消耗:14.1 MB, 在所有 C++ 提交中击败了90.20%的用户
*/

在排序数组中查找数字I

排序数组第一时间想二分法,同时根据二分法比较时有没有“=”,即nums[middle]<targetnums[middle]<=target的不同,指针会停在不同的位置,我们设置两次二分,得到左边界和右边界就好了。

1
统计一个数字在排序数组中出现的次数。
1
2
3
4
5
6
7
8
示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: 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
class Solution {
public:
int search(vector<int>& nums, int target) {
//二分法,判断条件的“=”能使得算法指针停在不同的边界
//分别得出target左边界和右边界即可知道有多少个
int i = 0, r = nums.size()-1;
if(r<0) return 0;
while(i<=r)
{
int middle = i + (r-i)/2;
if(nums[middle]<target)
i = middle+1;
else
r = middle-1;//如果找到target,会缩小右边界,继续往前找
//也就是说i是左边界,i是target第一次出现的位置(如果有)
}
if(i>=nums.size() || nums[i]!=target) return 0;//i越界或者i的位置不是target,说明找不到,提前返回
//越界是因为可能所有元素都比target小

int j = 0;
r = nums.size()-1;

while(j<=r)
{
int middle = j + (r-j)/2;
if(nums[middle]<=target)//如果找到target,会缩小左边界,继续往后找
j = middle+1;
else
r = middle-1;
//也就是说j是右边界,这个右边界是target之后的那个数,因为找到target会继续往后
}
return j-i;
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了90.78%的用户
内存消耗:12.8 MB, 在所有 C++ 提交中击败了82.86%的用户
*/

day19

0~n-1中缺失的数字

排序数组用二分,这些题其实就二分的判断条件改改就完事了,然后注意边界怎么缩小的,要不要-1,while结束是i<j还是i<=j。一般来讲,边界都是m+1和m-1,对应的结束条件就是i<=j。

1
2
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。
在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
1
2
3
4
5
6
7
8
示例 1:

输入: [0,1,3]
输出: 2
示例 2:

输入: [0,1,2,3,4,5,6,7,9]
输出: 8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int missingNumber(vector<int>& nums) {
int i=0, j=nums.size()-1;
while(i<=j)
{
int middle = i+(j-i)/2;
//索引k处的值要么大于k要么等于k
if(nums[middle]==middle)
i = middle+1;//middle和middle前都是正确的,缩小左边界
else//大于
j = middle-1;//middle处已经大于了,要么是middle要么往前,缩小右边界
//那么middle为什么能-1呢?这样不会跳过正确答案吗?
//这是因为如果middle是正确答案且-1跳过了,那么会一直i=middle+1,最后回到正确答案且结束循环
//同时如果不减一则while循环无法结束,卡在i=j处(一直执行j=middle=i)

}
return i;
}
};
/*
执行用时:16 ms, 在所有 C++ 提交中击败了53.30%的用户
内存消耗:16.6 MB, 在所有 C++ 提交中击败了94.64%的用户
*/

数组中数字出现的次数

这个问题特殊之处在于,除了出现一次的数字,其他数字都出现了两次,这个两次很关键,它可以通过异或运算来加工——两个相同的数异或为0,0异或其他数字为其他数字。因此,把这些出现了两次的数字都异或了,结果就是0,接着去异或一个出现一次的数字,那么异或的结果就是这个数字了。

然而问题还没有这么简单,这个数组有两个出现一次的数字。我们的想法是,如果能把这两个数字分分组,就像奇偶一样分成两组就好了,因为每一组其他的数字都出现两次(相同的当然在一组啦),那么分别对这两组异或就能得到两个答案了。

但这两个目标数字不一定一个是奇数一个是偶数,我们可以肯定的是它们数值不一样。我们接着从异或出发,因为我们在异或整个数组前不知道两个数字是什么,那么我们在异或整个数组后能得到一个数z,z相当于这两个数字异或,肯定有一位是1,记这一位是m(全0说明这两数相同了)。就像奇数偶数一样,它们只是最后一位不同,那么我们也可以根据这一位m来分组,第m位为1的一组,第m位为0的一组,这样就分成两组了,分别异或就好了。

1
2
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。
要求时间复杂度是O(n),空间复杂度是O(1)。
1
2
3
4
5
6
7
8
示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
示例 2:

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]
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
class Solution {
public:
vector<int> singleNumbers(vector<int>& nums) {
//先遍历异或找出z
int z = 0;//初始为0,因为0异或谁结果就是谁
for(int num : nums)
z ^= num;//注意这里是^=,z=z异或num

//找出z为1的第m位
int m = 1;//00000....00001
while((z&m) == 0)
m <<= 1;//如果与运算是0,说明还没到1的那位,m左移把1对过去,注意这里是<<=,m等于m左移一位

//现在知道m了,边分组边异或
int x=0, y=0;//初始化同z
for(int num : nums)
{
if(num&m)
x ^= num;//第m位为1的异或
else
y ^= num;//第m位为0的异或
//两个数字肯定不在一起异或,因为第m位不同
}
return vector<int> {x,y};

}
};
/*
执行用时:12 ms, 在所有 C++ 提交中击败了89.84%的用户
内存消耗:15.6 MB, 在所有 C++ 提交中击败了85.82%的用户
*/

day20

数组中数字出现的次数II

上一个问题是出现两次的数字异或是0,现在呢,出现三次的数字加起来,再模三就是0。因此所有数字加起来模三就是那个只出现一次的数字模三,但这只能求得余数,不过这至少给我们一些启发。

这个余数是小于3的,如果要用加法和余数表示这个数字的话,那么这个数字原来的那位数就必须小于3。由此联想到二进制的一位数,我们把所有的数字都看成二进制的话,它们相加就是每一位相加。对于那些出现三次的数字,全部来看的话,二进制上每个位都正好被3整除,因为数字要么这一位是0,要么就有三个1。最后我们把目标数字的二进制添加上去,因为要么是0要么是1,没有超出余数的范围,这样的话模三就可以了。

注意这里的二进制“相加”并不是真的相加,而是统计每个二进制位到底有多少个1,然后模三,余数就是目标数字二进制的对应位置的值。

总结一下就是:考虑数字的二进制形式,对于出现三次的数字,各二进制位出现的次数都是 3 的倍数。因此,统计所有数字的各二进制位中 1 的出现次数,并对 3 求余,结果则为只出现一次的数字。

1
在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
1
2
3
4
5
6
7
8
示例 1:

输入:nums = [3,4,3,3]
输出:4
示例 2:

输入:nums = [9,1,7,9,7,9,7]
输出:1
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
class Solution {
public:
int singleNumber(vector<int>& nums) {
//int最多只有三十二位,所以只需要统计32位上每一位的1的个数
vector<int> binary(32,0);

//遍历统计每一个num
for(int num:nums)
//对于每一个数,统记二进制位
for(int i=0;i<32;i++)//0表示最低位,往高位走
{
binary[i] += num&1;//取最后一位,如果是1就+1,如果是0就+0,数值对应就不用if-else了,直接加法
num >>= 1;//num右移一位
}

//现在要模三,取每一位数
int res = 0;
for(int i=0;i<32;i++)
{
//从高位来,这样res不断左移就往高处推了
//如果从低位开始,res不能左移不能右移,反而k每次要左移i次
//虽然影响不大,终究浪费点效率
int k = binary[31-i]%3;

//k要么是0要么是1,k总是在最低位,所以res要不断左移
res <<= 1;//先左移再取位,顺序反了的话最低位总是会左移...
res |= k;//res = res|k

}
return res;
}
};
/*
执行用时:32 ms, 在所有 C++ 提交中击败了66.58%的用户
内存消耗:15.6 MB, 在所有 C++ 提交中击败了89.42%的用户
*/

和为s的两个数字

前后双指针,老生常谈了。

优化的话,可把while里面if判等的条件放在while判断

1
2
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。
如果有多对数字的和等于s,则输出任意一对即可。
1
2
3
4
5
6
7
8
示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]
示例 2:

输入:nums = [10,26,30,31,47,60], target = 40
输出:[10,30] 或者 [30,10]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int i=0, j=nums.size()-1;
while(nums[i]+nums[j]!=target)
{
if(nums[i]+nums[j]>target)
j--;
else
i++;
}
return vector<int>{nums[i],nums[j]};
}
};
/*
执行用时:140 ms, 在所有 C++ 提交中击败了96.73%的用户
内存消耗:98.1 MB, 在所有 C++ 提交中击败了67.91%的用户
*/

day21

和为s的连续正整数序列

双指针一直往前滑动,维护一个滑动窗口就好了。这种方式实际上是在不断否定前面的序列,因为如果大了,那么右边界移动就更不可能了,所以只能左边界移动;如果小了就只移动右边界扩大,因为左边界起始点不动是可能的。

1
2
3
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
1
2
3
4
5
6
7
8
示例 1:

输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:

输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
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
class Solution {
public:
vector<vector<int>> findContinuousSequence(int target) {
//双指针
int i=1, j=2;//窗口的左边界和右边界,一直往前看就行
vector<vector<int>> res;
int count;
while(i<j)//i不能=j
{
count = (i+j)*(j-i+1)/2;
if(count==target)
{
vector<int> tmp;
for(int k=i;k<=j;k++)
tmp.push_back(k);
res.push_back(tmp);
i++;//i往后一位
j++;//i往后肯定更小,j一定要往后
}
else if(count<target)
j++;//小了就j++,扩大右边界
else
i++;//大了就i++,缩小左边界,减去小的值
}
return res;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:6.5 MB, 在所有 C++ 提交中击败了62.75%的用户
*/

翻转单词顺序

用个辅助栈咯,在遍历过程中用个string存单词字母,遇到空格说明单词存好了,放入栈里,同时这个string变成空。那么如果后面还是空格,string是空,就可以辨别出是不是连续的空格了,因为单词后的空格string不是空。

最后要拼接,再把多余的空格删除。string经常要删除最后一个字符,用pop_back()舒服点。

1
2
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。
为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",则输出"student. a am I"。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
示例 1:

输入: "the sky is blue"
输出: "blue is sky the"
示例 2:

输入: "  hello world!  "
输出: "world! hello"
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
示例 3:

输入: "a good   example"
输出: "example good a"
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
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
class Solution {
public:
string reverseWords(string s) {
stack<string> help;
string res = "";
string tmp = "";
//这轮遍历只记单词,空格一律过滤掉
for(int i=0;i<s.size();i++)
{
if(s[i]==' ' and tmp=="")//遇到连续的空格
continue;
else if(s[i]==' ' and tmp!="")//遇到第一个空格
{
help.push(tmp);
tmp = "";
}
else//最后一个单词不一定有空格,所以还要if一下
{

tmp+=s[i];//不是空格的话
if(i==s.size()-1)//最后的了
help.push(tmp);


}
}


while(!help.empty())
{
res += help.top();
help.pop();
res += " ";
}
//最后会多一个空格
//可以用substr(0,len-1)左闭右开
//可以用earse(len-1)
//可以用pop_back()
res.pop_back();
return res;
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了77.87%的用户
内存消耗:8.2 MB, 在所有 C++ 提交中击败了50.94%的用户
*/

day22

左旋转字符串

简单

1
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
1
2
3
4
5
6
7
8
示例 1:

输入: s = "abcdefg", k = 2
输出: "cdefgab"
示例 2:

输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
string reverseLeftWords(string s, int n) {
int size = s.size();
string tmp = s.substr(0,n);//把前面n个记录下来
for(int i=0;i<size-n;i++)
s[i] = s[i+n];//往前挪
for(int i=size-n;i<size;i++)
s[i] = tmp[i-size+n];//把后面填上去
return s;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:7.2 MB, 在所有 C++ 提交中击败了52.27%的用户
*/

day23

滑动窗口的最大值

记住:使用一个辅助的非严格递减的双端队列,先进先出,头部元素就是窗口的最大值。

这题正常想挺难想的,关键词是顺序遍历(滑动)、最值,实际上跟min栈很像,也是以一定顺序push和pop。min栈用的是辅助栈,滑动窗口是先进先出的,因此可以考虑用一个辅助的单调双端队列。

这个队列是维持窗口可能的最大值的,要怎么设计的呢?我们的窗口向后移动时,会移除最前面的元素,添加后面的元素。如果后的元素较大,那么更前的比它小的元素一定不可能再成为之后某个窗口的最大值,因为它们比后面的元素先出去,还比后面的元素小。

基于这样的思想,就可以在插入单调队列时把比这个元素小的都移除,这些被移除的元素在nums数组的位置肯定在插入的元素前。那么怎么移除呢,完全遍历的话就浪费时间,如果能维持单调递减,就可以从后往前比较大小和pop了,

在窗口移动的过程中,我们不仅增加了一个元素还减少了一个元素,如果这个元素在单调队列里面,我们需要把它删掉。如果它不是前一个窗口的最大值,那么它一定不在单调队列了,因为它是前一个窗口的第一个元素,没有后面元素大肯定不在;如果它是前一个窗口的最大值,那它在队列头部,pop掉就完事了。这能保持队列内的候选者都是当前窗口内的元素。

这样,单调队列的第一个元素,即最大的元素一定是这个滑动窗口的最大值了。

1
给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:

滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
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
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
//单调队列,非严格递减
vector<int> res;
deque<int> deq;
deq.push_back(nums[0]);//初始化
//没形成窗口前,先维护双端队列
for(int i=1;i<k;i++)
{
//把比nums[i]小的都丢了,当然如果队列已经空了就不做了
while(!deq.empty() and nums[i]>deq.back())
deq.pop_back();
//把nums[i]添加进去
deq.push_back(nums[i]);
}
res.push_back(deq[0]);
//现在已经有一个滑动窗口了
//移向下一个滑动窗口
for(int i=1,j=k;j<nums.size();i++,j++)
{
//deq现在是上一个滑动窗口的单调队列
if(nums[i-1]==deq[0])
deq.pop_front();//如果移除的值是最大值,就把最大值移出单调队列

//把比nums[j]小的都丢了,当然如果队列已经空了就不做了
while(!deq.empty() and nums[j]>deq.back())
deq.pop_back();//在j之前的数还比j小的话,那么这些数不可能是最大值了,因为这些数走得早
//把nums[j]添加进去
deq.push_back(nums[j]);

/*
前面的操作能保证i-j窗口以外的元素已经绝对不在deq里了
因为j之后的还没添加,i之前的,如果i-1不是最大值,那么上一个窗口的最大值就在i-j-1之间
根据单调队列的实现,i之前的元素都不会在,如果i-1是最大值,那么i-1之前的元素都不会在
且i-1会被pop掉
*/
res.push_back(deq[0]);//添加最大值
}
return res;
}
};
/*
执行用时:212 ms, 在所有 C++ 提交中击败了32.23%的用户
内存消耗:125 MB, 在所有 C++ 提交中击败了34.95%的用户
*/

day24

n个骰子的点数

在现实中,我们计算概率的时候会从前n-1个骰子的结果推向第n个骰子的结果,因为第n个骰子无非是1-6,某个值s的概率就是前n-1个骰子产生s-i的概率除以6(i从1到6),然后这些s-i都可以到达s,因此把这些概率累加。这种递推的想法给我们动态规划的考虑。

动态规划f(n,x) = f(n-1,x-i)/6.0(i:1-6,累加)。

这是逆向的想法,我们想要得到一个f(n,x),要逆向推f(n-1,x-i)。但如果逆向,即从骰子为n一直向前推,当x<=6时都要做特殊处理。

因此改成正向的动态规划,从骰子为1开始,由于新增骰子的点数只可能为 1 至 6 ,因此概率 f(n−1,x) 仅与 f(n,x+1) , f(n,x+2), … , f(n,x+6) 相关。正向的递推就是当我们得到一个f(n,x),可以产生一部分f(n+1,x+i)的概率,对不同的x,累加这些x+i即可。

因而,遍历 f(n−1) 中各点数和的概率,并将其相加至 f(n) 中所有相关项,即可完成 f(n−1) 至 f(n) 的递推。

具体的:动态规划正向递推,从小到大遍历n个骰子,遍历每个骰子的每一个值,对每个值遍历下一个骰子的1-6的值。

1
2
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
1
2
3
4
5
6
7
8
示例 1:

输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
示例 2:

输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
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
class Solution {
public:
//动态规划f(n,x) = f(n-1,x-i)/6.0(i:1-6,累加)
//如果逆向,即从骰子为n一直向前推,当x<=6时都要做特殊处理
//因此改成正向的动态规划,从骰子为1开始,由于新增骰子的点数只可能为 1 至 6 ,因此概率 f(n−1,x) 仅与 f(n,x+1) , f(n,x+2), ... , f(n,x+6) 相关。
//因而,遍历 f(n−1) 中各点数和的概率,并将其相加至 f(n) 中所有相关项,即可完成 f(n−1) 至 f(n) 的递推。

vector<double> dicesProbability(int n) {
//点数范围是n-6n,个数是5n+1
//真实的值s是下标+1
vector<double> dp(6,1.0/6.0);//初始化,骰子1
for(int i=2;i<=n;i++)//遍历骰子数量
{
vector<double> tmp(5*i+1,0);//i个骰子时,有5i+1个值
for(int j=0;j<dp.size();j++)//对上一个骰子的每一个值,能对下一个骰子产生的值起作用
for(int k=0;k<6;k++)//下一个骰子的值是1-6,每个值概率是六分之一
tmp[j+k] += dp[j]/6.0;//第j+k个值可以由dp[j]产生,概率是六分之一。注意是+=,累加的,比如2+3和3+2点数都是5
dp = tmp;//如果还有循环,dp就代表上一个骰子的值的概率
}
return dp;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:6 MB, 在所有 C++ 提交中击败了94.82%的用户
*/

day25

扑克牌中的顺子

很自然地想到排序(顺子嘛),然后正常的想法是遍历0,然后知道0的数目(4个0直接true),再接下去遍历,如果后面遍历遇到重复就false,如果往后都是+1递增就继续,如果不是+1递增,就把当前值+1(同时把0的数目减少一个,相当于补充一个中间+1值),再看是不是+1,如果不是,再用一个0…这样下去,遍历完就true,0用完就false。

当有更直观的办法,如果知道0的数目,也判断了没有重复,那么这5张牌是顺子的充要条件是max-min<5。max是最大值,min是除0外的最小值。只要差值比5小,由于没有重复,那么0就可以填充在序列的中间,或者序列的外部(如果原来就是顺子,就填充在外部)

1
2
从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。
2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
1
2
3
4
5
6
7
8
9
10
示例 1:

输入: [1,2,3,4,5]
输出: True


示例 2:

输入: [0,0,1,2,5]
输出: True
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
class Solution {
public:
bool isStraight(vector<int>& nums) {

//先排序
quickSort(nums,0,4);//数组长度为5,就直接利用了
//统计大小王与判断重复
int count = 0;
//为什么只遍历前四个元素呢?
//原因是比较前后两个元素对头尾的下标有要求,不能越界,这里少遍历一个元素
//更重要的一点是,如果有四个大小王,那么无论如何也能形成顺子,最后一个元素不用看了
//如果没有四个大小王,说明第四个不是0,最后一个无论如何也不是0,没必要看
for(int i=0;i<4;i++)
{
if(nums[i]==0) count++;
else if(nums[i]==nums[i+1]) return false;//如果重复就false,注意要elseif,因为0是可以重复的
}
//四个王就结束了
if(count == 4) return true;
//这里很关键,如果没有重复,那么除去0后剩下的元素要形成顺子,它们的梯度小于5即可
return (nums[4]-nums[count]<5);
}

private:
void quickSort(vector<int>& nums, int l, int r)
{
if(l>=r) return;
int i=l, j=r;
while(i<j)
{
while(i<j && nums[j]>=nums[l]) j--;
while(i<j && nums[i]<=nums[l]) i++;

swap(nums[i],nums[j]);
}
swap(nums[i],nums[l]);//最后i停在比nums[l]小的元素,交换它们,把基准值放中间
quickSort(nums,l,i-1);
quickSort(nums,i+1,r);
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:9.7 MB, 在所有 C++ 提交中击败了92.27%的用户
*/

day26

圆圈中最后剩下的数字

约瑟夫环问题,用链表模拟会超时。约瑟夫环有数学上的递推公式,因此有动态规划的解法。

输入 n, m,记此约瑟夫环问题为 「n, m 问题」 ,设解(即最后留下的数字)为 f(n) ,则有:

「n, m 问题」:数字环为 0, 1, 2, …, n - 1,解为 f(n) ;
「n-1, m 问题」:数字环为 0, 1, 2, …, n - 2,解为 f(n-1);

对于「n, m 问题」,首轮删除环中第 m 个数字后,得到一个长度为 n - 1 的数字环。由于有可能 m > n ,因此删除的数字为 (m−1)%n ,删除后的数字环从下个数字(即 m%n )开始,设 t=m%n ,可得数字环:

t,t+1,t+2,…,0,1,…,t−3,t−2

删除一轮后的数字环也变为一个「n-1, m 问题」,观察以下数字编号对应关系:

「n−1,m问题」 「n,m问题」删除后
0 t+0
1 t+1
n-2 t-2

设「n-1, m 问题」某数字为 x ,则可得递推关系:x→(x+t)%n。

换而言之,若已知「n-1, m 问题」的解 f(n - 1) ,则可通过以上公式计算得到「n, m 问题」的解 f(n),即:

f(n)=(f(n−1)+t)%n=(f(n−1)+m%n)%n=**(f(n−1)+m)%n**


这个怎么理解呢?从正向递推去看,如果知道n-1问题的解,那么能对应到n问题的解。因为n问题首先要删除一个数变成n-1问题,而n问题的解和n-1问题的解从元素角度看必然是同一个(因为本来就是同一个问题的不同过程),只是它们在不同的序列,拥有不同的值而已。那么我们只需要把值做一次映射,就能从n-1问题的解的值映射到n问题的解的值。从n问题到n-1问题,删除了(m-1)%n,新序列每个值都减了或加了(m-1)%n+1,反过来只要加回去就可以了(加什么值最好用一个例子推)。从n=1时开始往后推,不断映射到下一个规模的解返回即可(因为最终返回的解的值是在n问题序列的解)。

1
2
3
4
5
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。
求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,
则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3
1
2
3
4
5
6
7
8
示例 1:

输入: n = 5, m = 3
输出: 3
示例 2:

输入: n = 10, m = 17
输出: 2
1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int lastRemaining(int n, int m) {
int x = 0;//n=1时的结果
for(int i=2;i<=n;i++)//从n=1递推到n=2,一直递推到n
x = (x+m)%i;
return x;
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了93.52%的用户
内存消耗:5.7 MB, 在所有 C++ 提交中击败了94.35%的用户
*/

day27

股票的最大利润

动态规划,不谈了,ez

1
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
1
2
3
4
5
6
7
8
9
10
11
示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int maxProfit(vector<int>& prices) {
int min = 0x7fffffff;
int profit = 0;
for(int num : prices)
{
//如果今天的价格比存储的最小值小,则更改最小值
if(num<=min)
min = num;//不可能在今天卖出,因为一定是亏的或者是0
else//如果大的话可以卖一下,min存储了前几天的最小值
{
profit = max(profit,num-min);//更新利润
}
}
return profit;
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了88.00%的用户
内存消耗:12.4 MB, 在所有 C++ 提交中击败了79.39%的用户
*/

day28

求1+2+…+n

首先不能用while等循环,显然就用递归了。然后当n>0才递归,因此要判断,但是不能用if那些,可以考虑用布尔逻辑的短路效应。

1
求 1+2+...+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
1
2
3
4
5
6
7
8
示例 1:

输入: n = 3
输出: 6
示例 2:

输入: n = 9
输出: 45
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int sumNums(int n) {
//n>0时,n是真,根据与表达式,还要看后面的真假,所以会做 n+=sumNums(n-1);
//n=0时,整个表达式已经是假了,不会做后面的运算了,递归终止。
//整个表达式的布尔真假没有意义,并不需要,只需要用来根据n的值决定做不做递归即可,最后返回n
n && (n += sumNums(n-1));
return n;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:6.1 MB, 在所有 C++ 提交中击败了25.90%的用户
*/

day29

不用加减乘除做加法

加法器原理,用位运算实现。a+b相当于无进位加法加上进位。假设无进位加法结果是n,进位是c,则a+b=n+c。无进位加法n=a^b,即两数异或,位全1或全0这一位的结果都是0。进位可以用与运算,c=(a&b)<<1,因为是进位所以要左移。

在位运算后,我们就变成了要计算n+c,这还是一个加法,因此又要一轮无进位加法和进位,像递归一样不断往复下去,直到没有进位就可以停止了。

1
写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。
1
2
输入: a = 1, b = 1
输出: 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
//分为无进位加法结果n和进位结果c,则a+b=n+c
int add(int a, int b) {
//位运算,把a当结果,把b加到a上
int c;//存储进位,无进位加法结果直接存a上,相当于n=a^b,下一轮两个加数是n和c,则n=a,b=c;n可以用a替代
while(b)//b不为0时进行,b为0说明加完了
{
//c++不支持负数左移,要转unsigned,因为整个过程只是bit串运算,不用管正负,不需要c++去解释正负
c = (unsigned int)(a&b) << 1;//两数的每个bit的进位

//无进位加法,加完再和进位加就可以
a = a^b;//对于加法,都是1就进位,结果是0,都是0那结果也是0。都是1时进位在c那
b=c;//c已经左移过了,本身是要a+c,但也不能用加法,所以还是要异或,a+c就像a+b一样,一直循环直到没进位
}
return a;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:5.7 MB, 在所有 C++ 提交中击败了69.28%的用户
*/

day30

构建乘积数组

不能用除法,所以考虑乘法,再考虑性能的话就要考虑乘法的递推。累乘的中间是割裂的,但割裂的这个i是递增的,所以这里面也有些规律可循。

再来思考一下,从形式上看有递推的效果,可以考虑动态规划,但是递推总是割裂的,差一点,原因是还要找规律。

- - - - - - -
B[0]= 1 A[1] A[2] A[n-1] A[n]
B[1]= A[0] 1 A[2] A[n-1] A[n]
B[2]= A[0] A[1] 1 A[n-1] A[n]
B[n-1]= A[0] A[1] A[2] 1 A[n]
B[n]= A[0] A[1] A[2] A[n-1] 1

整个累乘可以分成上三角矩阵和下三角矩阵,B[i]的计算可以分成两次计算。前面动态规划差一点的原因就是中间有断裂,那么可以分别从两个三角来。比如先计算下三角的部分,那么B[0] = 1; B[i] = B[i-1]*A[i-1];,这样子迭代就计算好了下三角,注意我们是迭代的递推的,因此要从小的开始慢慢乘起来,所以在回头计算上三角时,是从下往上的,从A[n]累乘到A[1],从B[n-1]到B[0]。

1
2
给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 
即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。
1
2
输入: [1,2,3,4,5]
输出: [120,60,40,30,24]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
vector<int> constructArr(vector<int>& a) {
int n = a.size();
if(n==0) return vector<int>{};
vector<int> B(n,0);
B[0] = 1;
//下三角递推
for(int i =1;i<n;i++)
B[i] = B[i-1]*a[i-1];
int tmp = 1;//不能用B[i-1]来递推了,用个tmp保存递推的中间值
//上三角递推
for(int i =n-2;i>=0;i--)
{
tmp *= a[i+1];
B[i] *= tmp;
}
return B;
}
};
/*
执行用时:16 ms, 在所有 C++ 提交中击败了86.75%的用户
内存消耗:23.7 MB, 在所有 C++ 提交中击败了91.26%的用户
*/

day31

今天忘记了,明天补一道/(ㄒoㄒ)/~~

day32

把字符串转换成整数

主要是越界的处理,要提前去判断。先说一下具体的操作:

  • 先把开头的空格遍历过去
  • 获得第一个非空格字符,如果非数字和正负号,返回0
  • 如果是负号,存储sign变量;正号同理
  • 如果是数字,说明是正号,正号可以不显式出现,因此默认情况下sign是正号。
  • 然后开始处理连续的数字,前面存储的是res,当前数字是num,num=str[i]-‘0’,res = res*10+num。在此过程中,要提前判断:
    • 如果遇到非数字字符就直接break,返回res和正负
    • 如果是数字字符,要判断拼接后会不会溢出,一个是res是否大于max/10,如果大于的话*10就溢出了。最恰好的情况是res==max/10,如果num>7就溢出了,根据正负号返回max或min。因为8的时候是正的溢出,返回max;但8不是负的溢出,但是此时的值也是min,不用再额外判断了。这种判断方式对于判断溢出、返回max和min很有效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;
假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0。

说明:
假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231,  231 − 1]。如果数值超过这个范围,请返回  INT_MAX (231 − 1) 或 INT_MIN (−231)
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
示例 1:

输入: "42"
输出: 42
示例 2:

输入: " -42"
输出: -42
解释: 第一个非空白字符为 '-', 它是一个负号。
  我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。
示例 3:

输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。
示例 4:

输入: "words and 987"
输出: 0
解释: 第一个非空字符是 'w', 但它不是数字或正、负号。
因此无法执行有效的转换。
示例 5:

输入: "-91283472332"
输出: -2147483648
解释: 数字 "-91283472332" 超过 32 位有符号整数范围。
  因此返回 INT_MIN (−231) 。
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
class Solution {
public:
int strToInt(string str) {
bool sign = true; //默认为正数
//先舍弃开头可能存在的空格
int i = 0;
while(i < str.size() && str[i] == ' ') i++;
//接着判断首个字符是否为正负号
if(str[i] == '-') {
sign = false; //该字符串片段为负数
i++; //移至下一个字符接着判断
}
else if(str[i] == '+') i++; //如果首个字符为‘+’则sign已经默认为true而无须更改,直接移动到下一位即可
//下面开始对非正负符号位进行判断
if(str[i] < '0' || str[i] > '9') return 0; //如果第一个正负号字符后的首个字符就不是数字字符(也可能第一个字符就不是正负号),那么直接返回0
int res = 0; //这里res用的int型,需要更加仔细考虑边界情况,但如果用long的话可以省去一些麻烦
int num; //用来单独存储单个字符转换而成的数字
int border = INT_MAX / 10; //用来验证计算结果是否溢出int范围的数据
while(i < str.size()){
if(str[i] < '0' || str[i] > '9') break; //遇到非数字字符则返回已经计算的res结果
if(res > border || res == border && str[i] > '7') //注意这句话要放在字符转换前,因为需要验证的位数比实际值的位数要少一位
//这里比较巧妙的地方在于 1. 用低于int型数据长度一位的数据border判断了超过int型数据长度的值 2. 将超过最大值和低于最小值的情况都包括了
return sign == true ? INT_MAX : INT_MIN;
//开始对数字字符进行转换
num = str[i] - '0';
res = res * 10 + num;
i++;
}
//最后结果根据符号添加正负号
return sign == true ? res : -res;
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了63.07%的用户
内存消耗:6 MB, 在所有 C++ 提交中击败了78.85%的用户
*/

二进制加法

手动模拟,逐位加。把顺序先倒过来会好一点。然后这里遍历的长度是较长的那个,同时判断,如果短的到边了那就是+0。

1
2
3
给定两个 01 字符串 a 和 b ,请计算它们的和,并以二进制字符串的形式输出。

输入为 非空 字符串且只包含数字 1 和 0。
1
2
3
4
5
6
7
8
示例 1:

输入: a = "11", b = "10"
输出: "101"
示例 2:

输入: a = "1010", b = "1011"
输出: "10101"
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
class Solution {
public:
string addBinary(string a, string b) {
string ans;
reverse(a.begin(), a.end());
reverse(b.begin(), b.end());

//carry即表示进位又表示结果
//carry表示进位和a、b位相加,其实就是三者中有多少个1。模二就是结果,结果是二就继续进位
int n = max(a.size(), b.size()), carry = 0;
for (size_t i = 0; i < n; ++i) {
carry += i < a.size() ? (a[i] == '1') : 0;//如果长度不够就是0,够的话用比较或者-'0'都行
carry += i < b.size() ? (b[i] == '1') : 0;
ans.push_back((carry % 2) ? '1' : '0');//当前位相加的结果,转char
carry /= 2;//下一位的进位,三者之和是2或3才有进位
}

if (carry) {//最后的处理
ans.push_back('1');
}
reverse(ans.begin(), ans.end());

return ans;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:6 MB, 在所有 C++ 提交中击败了85.36%的用户
*/

day33

前n个数字二进制中1的个数

动态规划,二进制中1的个数要想到 n&(n-1)能把n中最低的1变成0。这个变成0一方面让数字变小,一方面让1的个数少了1;也即:缩小了规模同时得到了数值关系。因此就有了递推式:bit[i] = bit[ i&(i-1) ] +1。

1
给定一个非负整数 n ,请计算 0 到 n 之间的每个数字的二进制表示中 1 的个数,并输出一个数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
示例 1:

输入: n = 2
输出: [0,1,1]
解释:
0 --> 0
1 --> 1
2 --> 10
示例 2:

输入: n = 5
输出: [0,1,1,2,1,2]
解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
vector<int> countBits(int n) {
vector<int> bits(n + 1);//初始化,bits[0] = 0;起始条件
for (int i = 1; i <= n; i++) {
bits[i] = bits[i & (i - 1)] + 1;//递推,i缩小了规模,尽管缩小的程序不知道,但因为i是递增的,所以<i的都解决了
}
return bits;
}
};
/*
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:7.6 MB, 在所有 C++ 提交中击败了72.76%的用户
*/

day34

单词长度的最大乘积

传统的暴力解法:遍历每个字符串对,然后再看两个字符串有没有相同字母。

先思考下一定要遍历字符串对吗,有没有递推的方式?答案是没有,因为这里不同的字符串对前后文没有关系,没有什么能够保存的状态,无法递推或分治,每对字符串都是新状态,所以一定要遍历所有字符串。同时要判断重复,就又得遍历两个字符串的字母,时间复杂度是大于n方的。

采取空间换取时间的方式,利用一个额外空间把字符串是否重复的信息存取。注意不能遍历字符串对去获取信息,这样就没有差别了。因此,要对每个字符串自身获取信息,同时利用这个信息在O(1)的复杂度判断有无重复。

O(1)的复杂度值得我们去考虑数学运算或位运算,尤其是判断重复会想到哈希表,也就想到映射。因此可以把字符串的字母映射到26位长的比特串上,如果有对应字母,对应的位置就是1。由于最多26位,所以可以用单个int来保存这个信息,也就是掩码。在判重时,两个掩码进行与运算,如果结果为0说明没有相同字母。

核心是:利用位掩码判断两个字符串是否有相同字符(进行与运算)。

1
2
给定一个字符串数组 words,请计算当两个字符串 words[i] 和 words[j] 不包含相同字符时,它们长度的乘积的最大值。
假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回 0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
示例 1:

输入: words = ["abcw","baz","foo","bar","fxyz","abcdef"]
输出: 16
解释: 这两个单词为 "abcw", "fxyz"。它们不包含相同字符,且长度的乘积最大。
示例 2:

输入: words = ["a","ab","abc","d","cd","bcd","abcd"]
输出: 4
解释: 这两个单词为 "ab", "cd"。
示例 3:

输入: words = ["a","aa","aaa","aaaa"]
输出: 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
class Solution {
public:
int maxProduct(vector<string>& words)
{
int length = words.size();
vector<int> masks(length);//掩码,初始化为0。一个掩码由一位int表示

//遍历所有字母
for (int i = 0; i < length; i++) //遍历所有字符串,words[i]对应masks[i]
{
string word = words[i];
int wordLength = word.size();
for (int j = 0; j < wordLength; j++)//遍历某个字符串,每个掩码假想有26位对应26个字母
{
masks[i] |= 1 << (word[j] - 'a');//获取是哪个字母,然后把1左移到对应位置上或起来。
}
}
int maxProd = 0;
for (int i = 0; i < length; i++)//遍历每个字符串对
{
for (int j = i + 1; j < length; j++)//j从i后面开始就可以了,降重
{
if ((masks[i] & masks[j]) == 0) //没有相同字母也就是掩码对应的1位置不同,与的结果是0
{
maxProd = max(maxProd, int(words[i].size() * words[j].size()));
}
}
}
return maxProd;
}
};
/*
执行用时:40 ms, 在所有 C++ 提交中击败了76.83%的用户
内存消耗:16.1 MB, 在所有 C++ 提交中击败了47.94%的用户
*/

day35

数组中和为0的三个数

固定元素+双指针

1
2
3
4
5
6
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,
同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 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
class Solution {
public:
//排序双指针
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
int n = nums.size();
sort(nums.begin(), nums.end());//排序
for(int i = 0; i < n - 2; i ++)//固定第一个元素,0到倒数第三个
{
if(i > 0 && nums[i] == nums[i - 1])//如果跟前面一个一样,那么就算找到了也是和前面答案一样的,重复了
continue;

int c = - nums[i];//c是剩下两个元素的和
//头尾双指针
int ll = i + 1, rr = n - 1; //j从i+1开始可以避免重复

while(ll < rr)//左右边界不重合,注意一定要遍历完,因为3+7和4+6都是答案
{
int sum = nums[ll] + nums[rr];
//移动头尾双指针找到第一个target
if(sum > c)
rr --;
else if(sum < c)
ll ++;
else
{
ans.push_back({nums[i], nums[ll], nums[rr]});//找到了就添加答案
//然后要把重复的都过滤掉,不然又是一组相同答案
while(ll < rr && nums[ll] == nums[++ ll]); //找到一个不重复的ll

while(ll < rr && nums[rr] == nums[-- rr]);
//过滤完后继续找下一个
}
}
}
return ans;
}
};
/*
执行用时:48 ms, 在所有 C++ 提交中击败了99.33%的用户
内存消耗:19.4 MB, 在所有 C++ 提交中击败了71.60%的用户
*/

day36

和大于等于target的最短子数组

这种连续子数组,尤其时牵扯到子数组的长度、连续和等等,可以用滑动窗口,更新边界即可。

1
2
3
4
给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。
如果不存在符合条件的子数组,返回 0 。
1
2
3
4
5
6
7
8
9
10
11
12
13
示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:

输入:target = 4, nums = [1,4,4]
输出:1
示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出: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
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int minlen = INT_MAX;
int i=0 ,j=0;//相等是因为可以只有1个元素满足
int n = nums.size();
int count = nums[0];
//j不能改成<n-1,因为j=n-1时虽然到最后了,不能退出,还要再判断更新一次minlen
while(i<=j and j<n)//i>j时返回,说明有len=1的满足
{
if(target <= count)//当前窗口符合,缩小左边界
{
minlen = min(minlen,j-i+1);//更新,窗口是i-j,大小是j-i+1
count -= nums[i];//左边界移动更新count
i++;
}
else
{
j++;//右边界移动,注意j++和i++顺序不同,因为一个是加新的,一个是减旧的
if(j==n)
break;//提前返回,当到最后(即使前面有符合的)不存在符合的子数组会越界
//因为当j到n-1时,在符合的窗口i++后可能不符合,j会尝试++,nums会越界
//一直不符合就更简单了,j一直++,但是又不能在while里改条件
count += nums[j];
}
}
return (minlen==INT_MAX)?0:minlen;
}
};
/*
执行用时:4 ms, 在所有 C++ 提交中击败了95.59%的用户
内存消耗:10.2 MB, 在所有 C++ 提交中击败了79.18%的用户
*/

day37

乘积小于K的子数组

滑动窗口,控制边界。这里的重点是对子数组个数的计数,如何不重复又如何不遗漏。这里子数组的连续性给了比较好的性质。当我们的窗口乘积比较大的时候,要缩小左边界,用乘积除以左边界的值更新乘积;而当乘积小于K的时候,这时就产生了子数组,且移动右边界。

我们从移动右边界的情形来看计数,当我们要更新计数时,上一个右边界(右边界-1)已经计数好了,那么当前的这个窗口只需要更新那些新产生的子数组的个数。这些新产生的子数组必然包含了右边界的元素(因为没包含右边界元素的子数组在上一次计数就算进去了)且包含了右边界的元素一定是新产生的子数组,那么我们从右边界往左边界数子数组的个数,子数组大小从1开始(根据子数组的连续性):[nums[j]],[nums[j],num[j-1]],…,[nums[j],…,nums[i]],个数就是窗口的大小j-i+1。这本质上是由于递推,上一次的子数组已经计算进去了。

因此,当乘积大时,更新左边界;当乘积符合时,更新计数,然后更新右边界。

1
给定一个正整数数组 nums和整数 k ,请找出该数组内乘积小于 k 的连续的子数组的个数。
1
2
3
4
5
6
7
8
9
10
示例 1:

输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。
示例 2:

输入: nums = [1,2,3], k = 0
输出: 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
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
//滑动窗口
int i=0, n=nums.size();
int pro = 1;
int res = 0;
//根据右边界来计数,每移动一次j都要更新值,所以用for的形式移动
for(int j=0;j<n;j++)
{
pro *= nums[j];
//i能等于j是为了能跳过某个本身就大于k的数,此时i=j+1,更新计数j-i+1也是0,然后就从下一个数开始
while(i<=j && pro >= k)//如果pro较大,移动左边界直到窗口符合条件
{
pro /= nums[i];
i++;
}
//有符合的窗口
res += j-i+1;
}
return res;
}
};
/*
执行用时:64 ms, 在所有 C++ 提交中击败了71.68%的用户
内存消耗:59.7 MB, 在所有 C++ 提交中击败了65.21%的用户
*/

day38

和为K的子数组

本以为这题和上一题差不多,但还是不一样,因为数组中的数字可以是负的,这把思路都改变了。前面小于的话,当找到合适的窗口,里面的更小的子数组都可以算进去,这样每次滑动窗口就可以了。但是由于这里数字是负的,并不知道该移动哪个边界,移动左边界窗口和既有可能增大也有可能减小。

如果没有负数,测试了几个例子,理论上滑动窗口也是可以解决的。而当前这种情况,就要用到一种更为通用的方式:前缀和(对应前一题为前缀积)。

这里使用额外的空间,保存一些前缀和pre[i],其中pre[i]表示从0-i所有元素的和。那么当我们每次遍历i时,能够通过之前的线性迭代很快获得当前的前缀和,这时要向前看x步找寻有没有和为k的一个子数组序列,本质上就是截出一段来,那么假设pre[i] - pre[j] =k,我们就截到了j-i这一段,注意由于我们是遍历过来的,所以j会比i小(这里的本质是,发现所有符合条件的以nums[i]结尾的子数组,既然是nums[i]结尾,那么其余元素一定是向前的)。

如果找到了这么一个pre[j] = pre[i] - k,就可以计数了。注意这样的pre[j]也许不只有一个,那么就可以用哈希表映射到次数,每次遍历完更新一下哈希表就好了。

注意前缀和是0的情况,因为为0时,也许有前缀和为0,也可以就是pre[i]而不减去其他前缀和,因此hash[0]本身应该多1,即初始化为1,其他为0。

1
给定一个整数数组和一个整数 k ,请找到该数组中和为 k 的连续子数组的个数。
1
2
3
4
5
6
7
8
9
示例 1:

输入:nums = [1,1,1], k = 2
输出: 2
解释: 此题 [1,1] 与 [1,1] 为两种不同的情况
示例 2:

输入:nums = [1,2,3], k = 3
输出: 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp;//key为前缀和,value为次数
mp[0] = 1;
int count = 0, pre = 0;
for (int x:nums) {
pre += x;//前缀和
if (mp.find(pre - k) != mp.end()) {//如果找得到pre-k的前缀和,则找到一组子数组
count += mp[pre - k];//mp的值是pre-k的前缀和出现的次数
}
mp[pre]++;//pre这个对应的前缀和+1,初始为0
}
return count;
}
};
/*
执行用时:48 ms, 在所有 C++ 提交中击败了99.09%的用户
内存消耗:35.1 MB, 在所有 C++ 提交中击败了72.84%的用户
*/

day39

0 和 1 个数相同的子数组

核心思想就是把0看成-1,这样个数相同的子数组的和就是0,这样问题归约为:最长和为0的连续子数组。这与上一题相似,只不过上一题是个数,这一题是最长长度。假设有前缀和count,位置为i,那么上一次出现的count处,位置为j,j+1——i这一段子数组就符合要求。因此使用一个哈希表,把前缀和映射到count出现的第一个位置(这样能使子数组最长)。具体的,这个第一个位置,只要我们在找到count时不更新即可,如果找不到count就更新。

1
给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
1
2
3
4
5
6
7
8
9
10
11

示例 1:

输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。
示例 2:

输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量 0 和 1 的最长连续子数组。
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
class Solution {
public:
int findMaxLength(vector<int>& nums) {
int maxLength = 0;
unordered_map<int, int> mp;//长度就映射到位置,个数就映射到个数
int counter = 0;
mp[counter] = -1;//如果此时前缀和为0的话,就是从头开始,这个要手动初始化
int n = nums.size();
for (int i = 0; i < n; i++) {
int num = nums[i];
if (num == 1) {//为1就累加前缀和
counter++;
}
else {//为0就前缀和减一
counter--;
}
if (mp.count(counter)) {//从下标prevIndex+1 到下标 i 的子数组中有相同数量的 0 和 1,该子数组的长度为i−prevIndex
int prevIndex = mp[counter];
maxLength = max(maxLength, i - prevIndex);
}
else {//如果counter 的值在哈希表中不存在,则将当前余数和当前下标 i 的键值对存入哈希表中。
mp[counter] = i;//第一次出现count的位置
}
}
return maxLength;
}
};
/*
执行用时:100 ms, 在所有 C++ 提交中击败了78.11%的用户
内存消耗:81.7 MB, 在所有 C++ 提交中击败了74.39%的用户
*/

day40

左右两边子数组的和相等

很简单的一道题,实际上就是遍历i,看每个i能不能当中心下标,那么在遍历的时候,累加前缀和和累减后缀和就可以判断了

1
2
3
4
5
6
7
给你一个整数数组 nums ,请计算数组的 中心下标 。

数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。

如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。

如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
示例 1:

输入:nums = [1,7,3,6,5,6]
输出:3
解释:
中心下标是 3 。
左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11 ,
右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11 ,二者相等。
示例 2:

输入:nums = [1, 2, 3]
输出:-1
解释:
数组中不存在满足此条件的中心下标。
示例 3:

输入:nums = [2, 1, -1]
输出:0
解释:
中心下标是 0 。
左侧数之和 sum = 0 ,(下标 0 左侧不存在元素),
右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 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
class Solution {
public:
int pivotIndex(vector<int>& nums) {
//实际上就是遍历i,看每个i能不能当中心下标
//那么在遍历的时候,累加前缀和和累减后缀和就可以判断了
int after = 0;
int pre = 0;
int res = -1;
for(int num:nums)
after += num;
for(int i=0;i<nums.size();i++)
{
after -= nums[i];//首先要减去这个值,把i空出来
if(pre == after)//然后pre先别加再判断
{
res = i;
break;//找最左边的
}

pre += nums[i];//往后移,pre填上
}
return res;
}
};
/*
执行用时:12 ms, 在所有 C++ 提交中击败了97.48%的用户
内存消耗:30.1 MB, 在所有 C++ 提交中击败了92.36%的用户
*/