C语言实现简单的WebServer服务器

C语言实现简单的WebServer服务器


基于TCP的套接字通信

在这里插入图片描述       这是一个单线程流程,服务器创建用于监听的套接字,绑定本地的ip和端口,listen函数去监听绑定的端口。
      如果有客户端进行连接,服务器端就可以和发起连接的客户端建立连接,连接建立成功会生成一个用于通信的套接字。用于监听的套接字和用于通信的套接字是不一样的。监听的套接字用于建立连接,通信的套接字用于数据交互。用于数据交互的read和write都是阻塞函数,在单线程下面,一个服务器想和多客户端进行通信,肯定是做不到的,因为accept,read,write都是阻塞的。
      为了使服务器可以正常的与多个客户端建立连接,并进行数据交互,需要用到多线程,多线程中的主线程负责建立连接(调用accept),子线程负责数据通信。
      多线程切换有一定的开销,因此引入非阻塞 I/O。非阻塞 I/O 不会将进程挂起,调用时会立即返回成功或错误,因此可以在一个线程里轮询多个文件描述符是否就绪。但是这种做法缺点是,每次发起系统调用,只能检查一个文件描述符是否就绪。当文件描述符很多时,系统调用的成本很高
      IO多路复用,也就是select,poll,epoll,可以通过一次系统调用,检查多个文件描述符的状态,相比于非阻塞 I/O,在文件描述符较多的场景下,避免了频繁的用户态和内核态的切换,减少了系统调用的开销。在IO多路复用中,阻塞是由内核实现的,自己编写的代码可以少许多不必要的阻塞。
      在单线程下,只用IO多路复用,没办法同时处理两件事情,为了提高效率,一般采用多线程+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
1.	在终端输入 启动程序 端口 和 程序主目录。
2. 启动监听套接字initListenFd(unsigned short port):
a) 创建监听fd,采用IPv4,TCP协议
b) 设置端口复用:如果程序服务器是主动断开连接的一方,会有一个2msl的等待时长,为了确认客户端已经收到我断开确认ack
c) 绑定IP和端口
d) 设置监听
e) 返回监听套接字lfd
3. 启动服务器程序epollRun(int lfd):
a) 创建epoll 树的根节点
b) lfd上树:上树用的是epoll_ctl函数
c) while true不停地检测是否有事件到来,根据epoll_wait返回的数组中的fd文件描述符,判断事件是连接请求,还是数据通信请求:
i. 如果是连接请求,调用acceptClient(lfd, epfd),建立新的连接。
ii. 如果是数据通信请求,调用recvHttpRequest(cfd, epfd),以http协议的方式传递消息。

acceptClient(lfd, epfd):
1. 建立连接,调用accept函数。
2. 设置非阻塞模式,非阻塞说的是文件描述符,默认得到的cfd是阻塞的,用fcntl修改文件描述符的属性。
3. cfd添加到epoll中,设置边沿触发方式。


recvHttpRequest(cfd, epfd):
1. 读取客户端发送过来的http请求头:
2. 判断数据是否被接收完毕:
a) if (len == -1 && errno == EAGAIN):证明有数据
解析请求行:parseRequestLine(const char* line, int cfd):
i. sscanf拆分字符串,得到请求方法和请求路径,仅处理get请求:
ii. 对请求路径中的中文进行处理,否则会乱码
iii. 判断文件路径是指向目录,还是文件,或者文件不存在:
1. 若文件路径不存在,发送404.html
2. 若文件路径是目录,则发送html的头部,然后发送格式化的目录列表,也是符合html格式。
3. 若文件路径指向具体文件,则分析文件类型,然后发送具体文件。
由于通信的文件描述符是非阻塞的,用sendfile发送文件的时候要处理返回值,
不断根据偏移量去发送文件,直到文件发送完毕,否则大文件的传输会出现问题。
cfd去读取发送数据内存的时候,是非阻塞的,读数据块速度很快,读到文件末尾偏移量之后,再进行while循环读取的时候
ret返回值为-1,errno == EAGAIN代表没有数据,可以再次进行尝试。

off_t offset = 0;
int size = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
while (offset < size) // 如果偏移量小于size,则表示文件没有发送完,继续发送
{
// 通信的文件描述符是非阻塞的
int ret = sendfile(cfd, fd, &offset, size - offset);
printf("ret value: %d\n", ret);
if (ret == -1 && errno == EAGAIN) // EAGAIN的意思是没有数据,可以再次进行尝试
{
printf("没数据...\n");
}
}
close(fd);
b) 否则说明客户端断开了连接,要及时的将cfd下树,并且关闭对应文件描述符

多线程服务流程

单线程的服务器模型中,主程序会不断阻塞的进行连接和数据通信两项工作,两项工作和主线程不独立,当连接请求比较多的时候,效率相对较低。
多线程的处理方法:在建立连接 acceptClient(lfd, epfd) 和 数据通信模块 recvHttpRequest(cfd, epfd)两部分,都开辟新的线程去做,让子线程去处理动作。
注意要在项目的输入,库依赖项中输入pthread,否则linux链接的时候找不到。

main.c代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include"Server.h"
#include<unistd.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
if (argc < 3) {
printf("./a.out port path\n");
return -1;
}

unsigned short port = atoi(argv[1]);
// 切换服务器的工作目录
chdir(argv[2]);

// 初始化监听的套接字
int lfd = initListenFd(port);

// 启动服务器程序
epollRun(lfd);

return 0;
}

Server.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
#pragma once

// 初始化监听的文件描述符
int initListenFd(unsigned short port);

// 启动epoll
int epollRun(int lfd);

// 和客户端建立连接
// int acceptClient(int lfd, int epfd);
void* acceptClient(void* arg);

// 接收http请求
// int recvHttpRequest(int cfd, int epfd);
void recvHttpRequest(void* arg);

// 解析请求行
int parseRequestLine(const char* line, int cfd);

// 发送文件
int sendFile(const char* fileName, int cfd);

// 发送响应头(状态行和响应头)
/**
* cfd 通信文件描述符
* status 状态码
* descr 状态描述
* type 描述数据格式
* length 数据库长度 若为-1,则告诉浏览器去计算长度
*/
int sendHeadMsg(int cfd, int status, const char* descr, const char* type, int length);

// 获取文件类型,已经有,不用再写
const char* getFileType(const char* name);

// 发送目录
int sendDir(const char* dirName, int cfd);

// 将数字从十六进制转换成十进制
int hexToDec(char c);

// 解码,解决中文乱码问题,from传入参数,to传出参数
void decodeMsg(char* to, char* from);

Server.c代码:

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

struct FdInfo
{
int fd;
int epfd;
pthread_t tid;
};


int initListenFd(unsigned short port )
{
/**
* 1. 创建监听的fd
* AF_INET:基于IPv4协议
* SOCK_STREAM:采用流式协议,即tcp协议
*/
int lfd = socket(AF_INET, SOCK_STREAM,0);
if (lfd == -1) {
perror("socket");
return -1;
}

// 2. 设置端口复用:如果程序服务器是主动断开连接的一方,会有一个2msl的等待时长,为了确认客户端已经收到我断开确认ack,
// 2msl之后才能释放端口
int opt = 1;
int ret = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt));
if (ret == -1) {
perror("setsockopt");
return -1;
}

// 3. 绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET; // 地址协议ipv4
addr.sin_port = htons(port); // 指定端口,指定的网络字节序为大端,需要进行转换
addr.sin_addr.s_addr = INADDR_ANY;
ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
perror("bind");
return -1;
}

// 4. 设置监听
// 128代表在监听过程中一次性能监听多少个连接请求
ret = listen(lfd,128);
if (ret == -1) {
perror("listen");
return -1;
}

// 5. 返回fd
return lfd;
}

int epollRun(int lfd)
{
// 1. 创建epoll 树的根节点
int epfd = epoll_create(1);
if (epfd == -1) {
perror("epoll_create");
return -1;
}

// 2. lfd上树:上树用的是epoll_ctl函数,epoll_ctl函数功能强大,第二个参数是表示当前对epoll树做什么操作
// 上树的时候,第二个参数是add
struct epoll_event ev;
ev.data.fd = lfd;
ev.events = EPOLLIN; //检测读事件

int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1) {
perror("epoll_ctl");
return -1;
}

// 3. 检测
struct epoll_event evs[1024];

while (1) {
// 若有事件连接,会存到evs中,并返回有多少个值为num。
int size = sizeof(evs) / sizeof(struct epoll_event);
int num = epoll_wait(epfd, evs, size, -1); //最后一个参数为-1,没有连接就一直阻塞
for (int i = 0; i < num; i++) {
struct FdInfo* info = (struct FdInfo*)malloc(sizeof(struct FdInfo));
int fd = evs[i].data.fd;
info->epfd = epfd;
info->fd = fd;

if (fd == lfd) {
// 建立新连接,将新连接添加到epoll树上,添加之后,epoll_wait再检测的节点就变多了。
//acceptClient(lfd, epfd);
pthread_create(&info->tid, NULL, acceptClient, info);
}
else {
// 数据通信,主要接收对端的数据,格式为http协议格式
//recvHttpRequest(fd, epfd);
pthread_create(&info->tid, NULL, recvHttpRequest, info);
}
}
}
return 0;
}
// int acceptClient(int lfd, int epfd);
void* acceptClient(void* arg)
{
struct FdInfo* info = (struct FdInfo*)arg;
/**
* 1. 建立连接,调用accept函数
* accept 三个参数:
* 第一个参数:需要监听的文件描述符
* 第二个参数:传出参数,用来保存客户端的ip和端口信息,这里不需要保存
* 第三个参数:计算第二个参数的大小
*/
int cfd = accept(info->fd, NULL, NULL);
if (cfd == -1) {
perror("accept");
return NULL;
}

// 2. 设置边沿非阻塞模式,非阻塞说的是文件描述符,默认得到的cfd是阻塞的,用fcntl修改文件描述符的属性
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);

// 3. cfd添加到epoll中:
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET; //检测读事件,EPOLLET设置为边沿触发

int ret = epoll_ctl(info->epfd, EPOLL_CTL_ADD, cfd, &ev);
if (ret == -1) {
perror("epoll_ctl");
return NULL;
}
return NULL;
}

// int recvHttpRequest(int cfd, int epfd);
void recvHttpRequest(void* arg)
{
struct FdInfo* info = (struct FdInfo*)arg;
int len = 0, total = 0;
char tmp[1024] = { 0 };
char buf[4096] = { 0 }; //用来存储客户端发过来的整个数据,不够长也没事,只要读出来请求头其实就可以了。
while ((len = recv(info->fd, tmp ,sizeof tmp, 0)) > 0) {
if (total + len < sizeof buf) {
memcpy(buf+total, tmp, len);
}
total += len;
}

// 判断数据是否被接收完毕
if (len == -1 && errno == EAGAIN) {
//解析请求行,另写一个函数,这里只解析请求头
char* pt = strstr(buf, "\r\n");
int reqLen = pt - buf;
buf[reqLen] = '\0';
parseRequestLine(buf, info->fd);
}
else if (len == 0) {
// 客户端断开了连接
epoll_ctl(info->epfd, EPOLL_CTL_DEL, info->fd, NULL);
close(info->fd);
}
else {
perror("recv");
}
return NULL;
}

int parseRequestLine(const char* line, int cfd)
{
// 解析请求行
// sscanf拆分字符串
char method[12]; // get or post
char path[1024];
sscanf(line, "%[^ ] %[^ ]", method, path);
// 只处理get请求
if (strcasecmp(method, "get") != 0) {
return -1;
}
decodeMsg(path, path); //处理中
// 处理静态资源(目录或文件)
char* file = NULL;
if (strcmp(path, "/") == 0)
{
file = "./";
}
else
{
file = path + 1;
}
// 获取文件属性,判断是目录还是文件
struct stat st;
int ret = stat(file, &st);
if (ret == -1) {
// 文件不存在, 404
sendHeadMsg(cfd, 404, "Not Found", getFileType(".html"), -1);
sendFile("404.html", cfd); //在当前目录下
return 0;
}
else if(S_ISDIR(st.st_mode)) {
// 是目录
sendHeadMsg(cfd, 200, "OK", getFileType(".html"), -1);
sendDir(file, cfd);
}
else {
// 请求的是文件,发送文件
sendHeadMsg(cfd, 200, "OK", getFileType(file), st.st_size);
sendFile(file, cfd);
}
return 0;
}

int sendFile(const char* fileName, int cfd)
{
// 1.打开文件
int fd = open(fileName, O_RDONLY);
assert(fd > 0);
//while (1) {
// char buf[1024];
// int len = read(fd, buf, sizeof buf);
// if (len > 0) {
// send(cfd, buf, len, 0);
// usleep(10); //不要发送太快,给对端一个喘口气的时间
// }
// else if (len == 0) {
// break;
// }
// else {
// prror("read");
// }
//}

off_t offset = 0;
int size = lseek(fd, 0, SEEK_END);
lseek(fd, 0, SEEK_SET);
while (offset < size) // 如果偏移量小于size,则表示文件没有发送完,继续发送
{
// 通信的文件描述符是非阻塞的
int ret = sendfile(cfd, fd, &offset, size - offset);
printf("ret value: %d\n", ret);
if (ret == -1 && errno == EAGAIN) // EAGAIN的意思是没有数据,可以再次进行尝试
{
printf("没数据...\n");
}
}
close(fd);
return 0;
}

int sendHeadMsg(int cfd, int status, const char* descr, const char* type, int length)
{
// 状态行
char buf[4096] = { 0 };
sprintf(buf, "http/1.1 %d %s\r\n", status, descr); //版本 状态码 描述语言
// 响应头
sprintf(buf + strlen(buf), "content-type: %s\r\n", type);
sprintf(buf + strlen(buf), "content-length: %d\r\n\r\n", length);

send(cfd, buf, strlen(buf), 0);
return 0;
}

const char* getFileType(const char* name)
{
// a.jpg a.mp4 a.html
// 自右向左查找‘.’字符, 如不存在返回NULL
const char* dot = strrchr(name, '.');
if (dot == NULL)
return "text/plain; charset=utf-8"; // 纯文本
if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
return "text/html; charset=utf-8";
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
return "image/jpeg";
if (strcmp(dot, ".gif") == 0)
return "image/gif";
if (strcmp(dot, ".png") == 0)
return "image/png";
if (strcmp(dot, ".css") == 0)
return "text/css";
if (strcmp(dot, ".au") == 0)
return "audio/basic";
if (strcmp(dot, ".wav") == 0)
return "audio/wav";
if (strcmp(dot, ".avi") == 0)
return "video/x-msvideo";
if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
return "video/quicktime";
if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
return "video/mpeg";
if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
return "model/vrml";
if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
return "audio/midi";
if (strcmp(dot, ".mp3") == 0)
return "audio/mpeg";
if (strcmp(dot, ".ogg") == 0)
return "application/ogg";
if (strcmp(dot, ".pac") == 0)
return "application/x-ns-proxy-autoconfig";

return "text/plain; charset=utf-8";
}

int sendDir(const char* dirName, int cfd)
{
char buf[4096] = { 0 };
sprintf(buf, "<html><head><title>%s</title></head><body><table>", dirName);
struct dirent** namelist;
int num = scandir(dirName, &namelist, NULL, alphasort); //修改vs设置c语言的标准为GUN11
for (int i = 0; i < num; ++i)
{
// 取出文件名 namelist 指向的是一个指针数组 struct dirent* tmp[]
char* name = namelist[i]->d_name;
struct stat st;
char subPath[1024] = { 0 };
sprintf(subPath, "%s/%s", dirName, name); //拼接字符串
stat(subPath, &st);
if (S_ISDIR(st.st_mode))
{
// a标签 <a href="">name</a>
sprintf(buf + strlen(buf),
"<tr><td><a href=\"%s/\">%s</a></td><td>%ld</td></tr>",
name, name, st.st_size);
}
else
{
sprintf(buf + strlen(buf),
"<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
name, name, st.st_size);
}
send(cfd, buf, strlen(buf), 0);
memset(buf, 0, sizeof(buf));
free(namelist[i]); //释放内存
}
sprintf(buf, "</table></body></html>");
send(cfd, buf, strlen(buf), 0);
free(namelist);
return 0;
}

// 将数字从十六进制转换成十进制
int hexToDec(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;

return 0;
}

void decodeMsg(char* to, char* from)
{
for (; *from != '\0'; ++to, ++from)
{
// isxdigit -> 判断字符是不是16进制格式, 取值在 0-f
if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2]))
{
// 将16进制的数 -> 十进制 将这个数值赋值给了字符 int -> char
// B2 == 178
// 将3个字符, 变成了一个字符, 这个字符就是原始数据
*to = hexToDec(from[1]) * 16 + hexToDec(from[2]);

// 跳过 from[1] 和 from[2] 因此在当前循环中已经处理过了
from += 2;
}
else
{
// 字符拷贝, 赋值
*to = *from;
}

}
*to = '\0';
}




C语言实现简单的WebServer服务器
https://cauccliu.github.io/2024/03/25/C语言实现最简单的服务器/
Author
Liuchang
Posted on
March 25, 2024
Licensed under